Delta Builder Fluent API #4
7
.windsurf/workflows/delta-builder.md
Normal file
7
.windsurf/workflows/delta-builder.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
description: Update the current file to use delta builder
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace each delta instantiation with a fluent call to createDelta from delta builder
|
||||||
|
- pass creator and host as arguments to createDelta
|
||||||
|
- use setProperty where appropriate
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
RhizomeNode,
|
RhizomeNode,
|
||||||
Lossless,
|
Lossless,
|
||||||
Delta,
|
|
||||||
AggregationResolver,
|
AggregationResolver,
|
||||||
MinResolver,
|
MinResolver,
|
||||||
MaxResolver,
|
MaxResolver,
|
||||||
@ -10,6 +9,8 @@ import {
|
|||||||
CountResolver,
|
CountResolver,
|
||||||
AggregationType
|
AggregationType
|
||||||
} from "../src";
|
} from "../src";
|
||||||
|
import { createDelta } from "../src/core/delta-builder";
|
||||||
|
import { Delta } from "../src/core/delta";
|
||||||
|
|
||||||
describe('Aggregation Resolvers', () => {
|
describe('Aggregation Resolvers', () => {
|
||||||
let node: RhizomeNode;
|
let node: RhizomeNode;
|
||||||
@ -23,46 +24,22 @@ describe('Aggregation Resolvers', () => {
|
|||||||
describe('Basic Aggregation', () => {
|
describe('Basic Aggregation', () => {
|
||||||
test('should aggregate numbers using min resolver', () => {
|
test('should aggregate numbers using min resolver', () => {
|
||||||
// Add first entity with score 10
|
// Add first entity with score 10
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add second entity with score 5
|
// Add second entity with score 5
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity2', 'score', 5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity2",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add third entity with score 15
|
// Add third entity with score 15
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity3', 'score', 15, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity3",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 15
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const minResolver = new MinResolver(lossless, ['score']);
|
const minResolver = new MinResolver(lossless, ['score']);
|
||||||
const result = minResolver.resolve();
|
const result = minResolver.resolve();
|
||||||
@ -76,44 +53,20 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
test('should aggregate numbers using max resolver', () => {
|
test('should aggregate numbers using max resolver', () => {
|
||||||
// Add deltas for entities
|
// Add deltas for entities
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity2', 'score', 5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity2",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity3', 'score', 15, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity3",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 15
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const maxResolver = new MaxResolver(lossless, ['score']);
|
const maxResolver = new MaxResolver(lossless, ['score']);
|
||||||
const result = maxResolver.resolve();
|
const result = maxResolver.resolve();
|
||||||
@ -126,46 +79,22 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
test('should aggregate numbers using sum resolver', () => {
|
test('should aggregate numbers using sum resolver', () => {
|
||||||
// Add first value for entity1
|
// Add first value for entity1
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add second value for entity1 (should sum)
|
// Add second value for entity1 (should sum)
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 20, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add value for entity2
|
// Add value for entity2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity2', 'value', 5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity2",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sumResolver = new SumResolver(lossless, ['value']);
|
const sumResolver = new SumResolver(lossless, ['value']);
|
||||||
const result = sumResolver.resolve();
|
const result = sumResolver.resolve();
|
||||||
@ -176,46 +105,22 @@ describe('Aggregation Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should aggregate numbers using average resolver', () => {
|
test('should aggregate numbers using average resolver', () => {
|
||||||
// Add multiple values for entity1
|
// Add multiple scores for entity1
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 20, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Single value for entity2
|
// Single value for entity2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity2', 'score', 30, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity2",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 30
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const avgResolver = new AverageResolver(lossless, ['score']);
|
const avgResolver = new AverageResolver(lossless, ['score']);
|
||||||
const result = avgResolver.resolve();
|
const result = avgResolver.resolve();
|
||||||
@ -227,45 +132,21 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
test('should count values using count resolver', () => {
|
test('should count values using count resolver', () => {
|
||||||
// Add multiple visit deltas for entity1
|
// Add multiple visit deltas for entity1
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'visits', 1, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "visits"
|
|
||||||
}, {
|
|
||||||
localContext: "visits",
|
|
||||||
target: 1
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'visits', 1, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "visits"
|
|
||||||
}, {
|
|
||||||
localContext: "visits",
|
|
||||||
target: 1
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Single visit for entity2
|
// Single visit for entity2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity2', 'visits', 1, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity2",
|
|
||||||
targetContext: "visits"
|
|
||||||
}, {
|
|
||||||
localContext: "visits",
|
|
||||||
target: 1
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const countResolver = new CountResolver(lossless, ['visits']);
|
const countResolver = new CountResolver(lossless, ['visits']);
|
||||||
const result = countResolver.resolve();
|
const result = countResolver.resolve();
|
||||||
@ -279,84 +160,36 @@ describe('Aggregation Resolvers', () => {
|
|||||||
describe('Custom Aggregation Configuration', () => {
|
describe('Custom Aggregation Configuration', () => {
|
||||||
test('should handle mixed aggregation types', () => {
|
test('should handle mixed aggregation types', () => {
|
||||||
// Add first set of values
|
// Add first set of values
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'min_val', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "min_val"
|
|
||||||
}, {
|
|
||||||
localContext: "min_val",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'max_val', 5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "max_val"
|
|
||||||
}, {
|
|
||||||
localContext: "max_val",
|
|
||||||
target: 5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'sum_val', 3, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "sum_val"
|
|
||||||
}, {
|
|
||||||
localContext: "sum_val",
|
|
||||||
target: 3
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add second set of values
|
// Add second set of values
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'min_val', 5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "min_val"
|
|
||||||
}, {
|
|
||||||
localContext: "min_val",
|
|
||||||
target: 5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'max_val', 15, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "max_val"
|
|
||||||
}, {
|
|
||||||
localContext: "max_val",
|
|
||||||
target: 15
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'sum_val', 7, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "sum_val"
|
|
||||||
}, {
|
|
||||||
localContext: "sum_val",
|
|
||||||
target: 7
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new AggregationResolver(lossless, {
|
const resolver = new AggregationResolver(lossless, {
|
||||||
min_val: 'min' as AggregationType,
|
min_val: 'min' as AggregationType,
|
||||||
@ -375,46 +208,22 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
test('should ignore non-numeric values', () => {
|
test('should ignore non-numeric values', () => {
|
||||||
// Add numeric value
|
// Add numeric value
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add non-numeric value (string)
|
// Add non-numeric value (string)
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'name', 'test', 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'test'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add another numeric value
|
// Add another numeric value
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'score', 20, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sumResolver = new SumResolver(lossless, ['score', 'name']);
|
const sumResolver = new SumResolver(lossless, ['score', 'name']);
|
||||||
const result = sumResolver.resolve();
|
const result = sumResolver.resolve();
|
||||||
@ -427,18 +236,10 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
test('should handle empty value arrays', () => {
|
test('should handle empty value arrays', () => {
|
||||||
// Create entity with non-aggregated property
|
// Create entity with non-aggregated property
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'name', 'test', 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'test'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sumResolver = new SumResolver(lossless, ['score']);
|
const sumResolver = new SumResolver(lossless, ['score']);
|
||||||
const result = sumResolver.resolve();
|
const result = sumResolver.resolve();
|
||||||
@ -451,18 +252,10 @@ describe('Aggregation Resolvers', () => {
|
|||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
test('should handle single value aggregations', () => {
|
test('should handle single value aggregations', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 42, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 42
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const avgResolver = new AverageResolver(lossless, ['value']);
|
const avgResolver = new AverageResolver(lossless, ['value']);
|
||||||
const result = avgResolver.resolve();
|
const result = avgResolver.resolve();
|
||||||
@ -472,31 +265,15 @@ describe('Aggregation Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle zero values', () => {
|
test('should handle zero values', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 0, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 0
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sumResolver = new SumResolver(lossless, ['value']);
|
const sumResolver = new SumResolver(lossless, ['value']);
|
||||||
const result = sumResolver.resolve();
|
const result = sumResolver.resolve();
|
||||||
@ -506,31 +283,15 @@ describe('Aggregation Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle negative values', () => {
|
test('should handle negative values', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', -5, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: -5
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('test', 'host1')
|
||||||
creator: 'test',
|
.setProperty('entity1', 'value', 10, 'collection')
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const minResolver = new MinResolver(lossless, ['value']);
|
const minResolver = new MinResolver(lossless, ['value']);
|
||||||
const result = minResolver.resolve();
|
const result = minResolver.resolve();
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
|
|
||||||
describe('Lossless View Compose/Decompose', () => {
|
describe('Lossless View Compose/Decompose', () => {
|
||||||
let node: RhizomeNode;
|
let node: RhizomeNode;
|
||||||
@ -18,22 +18,14 @@ describe('Lossless View Compose/Decompose', () => {
|
|||||||
it('should compose and decompose simple entity deltas correctly', () => {
|
it('should compose and decompose simple entity deltas correctly', () => {
|
||||||
// Create simple entity deltas
|
// Create simple entity deltas
|
||||||
const nameDeltas = [
|
const nameDeltas = [
|
||||||
new Delta({
|
createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Alice Smith')
|
||||||
pointers: [
|
.buildV1(),
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
createDelta('test-creator', 'test-host')
|
||||||
{ localContext: 'name', target: 'Alice Smith' }
|
.addPointer('users', 'alice', 'email')
|
||||||
]
|
.addPointer('email', 'alice@example.com')
|
||||||
}),
|
.buildV1()
|
||||||
new Delta({
|
|
||||||
creator: 'test-creator',
|
|
||||||
host: 'test-host',
|
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'email' },
|
|
||||||
{ localContext: 'email', target: 'alice@example.com' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Ingest the deltas
|
// Ingest the deltas
|
||||||
@ -73,17 +65,13 @@ describe('Lossless View Compose/Decompose', () => {
|
|||||||
|
|
||||||
it('should handle multi-pointer relationship deltas correctly', () => {
|
it('should handle multi-pointer relationship deltas correctly', () => {
|
||||||
// Create a complex relationship delta
|
// Create a complex relationship delta
|
||||||
const relationshipDelta = new Delta({
|
const relationshipDelta = createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'relationships')
|
||||||
host: 'test-host',
|
.addPointer('partner', 'bob')
|
||||||
pointers: [
|
.addPointer('type', 'friendship')
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'relationships' },
|
.addPointer('since', '2020-01-15')
|
||||||
{ localContext: 'partner', target: 'bob' },
|
.addPointer('intensity', 8)
|
||||||
{ localContext: 'type', target: 'friendship' },
|
.buildV1();
|
||||||
{ localContext: 'since', target: '2020-01-15' },
|
|
||||||
{ localContext: 'intensity', target: 8 }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
node.lossless.ingestDelta(relationshipDelta);
|
node.lossless.ingestDelta(relationshipDelta);
|
||||||
|
|
||||||
@ -115,33 +103,21 @@ describe('Lossless View Compose/Decompose', () => {
|
|||||||
|
|
||||||
it('should handle reference relationships correctly', () => {
|
it('should handle reference relationships correctly', () => {
|
||||||
// Create entities first
|
// Create entities first
|
||||||
const aliceDelta = new Delta({
|
const aliceDelta = createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Alice')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
|
||||||
{ localContext: 'name', target: 'Alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const bobDelta = new Delta({
|
const bobDelta = createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'bob', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'bob', targetContext: 'name' },
|
|
||||||
{ localContext: 'name', target: 'Bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create friendship relationship
|
// Create friendship relationship
|
||||||
const friendshipDelta = new Delta({
|
const friendshipDelta = createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: 'test-host',
|
.addPointer('friend', 'bob', 'friends')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friend', target: 'bob', targetContext: 'friends' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
[aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d));
|
[aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d));
|
||||||
|
|
||||||
@ -171,14 +147,10 @@ describe('Lossless View Compose/Decompose', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve delta metadata correctly', () => {
|
it('should preserve delta metadata correctly', () => {
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Alice')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
|
||||||
{ localContext: 'name', target: 'Alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
node.lossless.ingestDelta(originalDelta);
|
node.lossless.ingestDelta(originalDelta);
|
||||||
|
|
||||||
@ -198,30 +170,18 @@ describe('Lossless View Compose/Decompose', () => {
|
|||||||
it('should handle multiple deltas for the same property', () => {
|
it('should handle multiple deltas for the same property', () => {
|
||||||
// Create multiple name changes for alice
|
// Create multiple name changes for alice
|
||||||
const nameDeltas = [
|
const nameDeltas = [
|
||||||
new Delta({
|
createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Alice')
|
||||||
pointers: [
|
.buildV1(),
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
createDelta('test-creator', 'test-host')
|
||||||
{ localContext: 'name', target: 'Alice' }
|
.addPointer('users', 'alice', 'name')
|
||||||
]
|
.addPointer('name', 'Alice Smith')
|
||||||
}),
|
.buildV1(),
|
||||||
new Delta({
|
createDelta('test-creator', 'test-host')
|
||||||
creator: 'test-creator',
|
.addPointer('users', 'alice', 'name')
|
||||||
host: 'test-host',
|
.addPointer('name', 'Alice Johnson')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
|
||||||
{ localContext: 'name', target: 'Alice Smith' }
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new Delta({
|
|
||||||
creator: 'test-creator',
|
|
||||||
host: 'test-host',
|
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'name' },
|
|
||||||
{ localContext: 'name', target: 'Alice Johnson' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
|
|
||||||
nameDeltas.forEach(d => node.lossless.ingestDelta(d));
|
nameDeltas.forEach(d => node.lossless.ingestDelta(d));
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import {
|
import {
|
||||||
RhizomeNode,
|
RhizomeNode,
|
||||||
Lossless,
|
Lossless,
|
||||||
@ -24,35 +25,19 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
const timestamp = 1000;
|
const timestamp = 1000;
|
||||||
|
|
||||||
// Simulate two writers updating the same property at the exact same time
|
// Simulate two writers updating the same property at the exact same time
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withId('delta-a')
|
||||||
host: 'host1',
|
.withTimestamp(timestamp)
|
||||||
id: 'delta-a',
|
.setProperty('entity1', 'score', 100, 'collection')
|
||||||
timeCreated: timestamp,
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer2', 'host2')
|
||||||
creator: 'writer2',
|
.withId('delta-b')
|
||||||
host: 'host2',
|
.withTimestamp(timestamp) // Same timestamp
|
||||||
id: 'delta-b',
|
.setProperty('entity1', 'score', 200, 'collection')
|
||||||
timeCreated: timestamp, // Same timestamp
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 200
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new LastWriteWins(lossless);
|
const resolver = new LastWriteWins(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -66,35 +51,19 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => {
|
test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => {
|
||||||
const timestamp = 1000;
|
const timestamp = 1000;
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer_z', 'host1') // Lexicographically later
|
||||||
creator: 'writer_z', // Lexicographically later
|
.withId('delta-a')
|
||||||
host: 'host1',
|
.withTimestamp(timestamp)
|
||||||
id: 'delta-a',
|
.setProperty('entity1', 'score', 100, 'collection')
|
||||||
timeCreated: timestamp,
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer_a', 'host2') // Lexicographically earlier
|
||||||
creator: 'writer_a', // Lexicographically earlier
|
.withId('delta-b')
|
||||||
host: 'host2',
|
.withTimestamp(timestamp) // Same timestamp
|
||||||
id: 'delta-b',
|
.setProperty('entity1', 'score', 200, 'collection')
|
||||||
timeCreated: timestamp, // Same timestamp
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 200
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new TimestampResolver(lossless, 'creator-id');
|
const resolver = new TimestampResolver(lossless, 'creator-id');
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -108,47 +77,24 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
const timestamp = 1000;
|
const timestamp = 1000;
|
||||||
|
|
||||||
// Multiple writers add values simultaneously
|
// Multiple writers add values simultaneously
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(1000)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'points', 10, 'collection')
|
||||||
timeCreated: timestamp,
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "points"
|
|
||||||
}, {
|
|
||||||
localContext: "points",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer2', 'host2')
|
||||||
creator: 'writer2',
|
.withTimestamp(1000) // Same timestamp
|
||||||
host: 'host2',
|
.setProperty('entity1', 'points', 20, 'collection')
|
||||||
timeCreated: timestamp,
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "collection",
|
|
||||||
target: "entity1",
|
|
||||||
targetContext: "points"
|
|
||||||
}, {
|
|
||||||
localContext: "points",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third writer adds another value
|
||||||
creator: 'writer3',
|
lossless.ingestDelta(createDelta('writer3', 'host3')
|
||||||
host: 'host3',
|
.withTimestamp(1000) // Same timestamp
|
||||||
timeCreated: timestamp,
|
.setProperty('entity1', 'points', 30, 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "points"
|
|
||||||
}, {
|
|
||||||
localContext: "points",
|
|
||||||
target: 30
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new SumResolver(lossless, ['points']);
|
const resolver = new SumResolver(lossless, ['points']);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -162,34 +108,20 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
describe('Out-of-Order Write Arrival', () => {
|
describe('Out-of-Order Write Arrival', () => {
|
||||||
test('should handle writes arriving out of chronological order', () => {
|
test('should handle writes arriving out of chronological order', () => {
|
||||||
// Newer delta arrives first
|
// Newer delta arrives first
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(2000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'value')
|
||||||
timeCreated: 2000, // Later timestamp
|
.addPointer('value', 'newer')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 'newer'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Older delta arrives later
|
// Older delta arrives later
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(1000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'value')
|
||||||
timeCreated: 1000, // Earlier timestamp
|
.addPointer('value', 'older')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 'older'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new LastWriteWins(lossless);
|
const resolver = new LastWriteWins(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -201,47 +133,26 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
|
|
||||||
test('should maintain correct aggregation despite out-of-order arrival', () => {
|
test('should maintain correct aggregation despite out-of-order arrival', () => {
|
||||||
// Add deltas in reverse chronological order
|
// Add deltas in reverse chronological order
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(3000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 3000,
|
.addPointer('score', 30)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 30
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(1000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(2000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 2000,
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new SumResolver(lossless, ['score']);
|
const resolver = new SumResolver(lossless, ['score']);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -261,19 +172,12 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
// Simulate multiple writers making rapid updates
|
// Simulate multiple writers making rapid updates
|
||||||
for (let writer = 0; writer < numWriters; writer++) {
|
for (let writer = 0; writer < numWriters; writer++) {
|
||||||
for (let write = 0; write < writesPerWriter; write++) {
|
for (let write = 0; write < writesPerWriter; write++) {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`)
|
||||||
creator: `writer${writer}`,
|
.withTimestamp(baseTimestamp + write)
|
||||||
host: `host${writer}`,
|
.addPointer('collection', 'entity1', 'counter')
|
||||||
timeCreated: baseTimestamp + write, // Small time increments
|
.addPointer('counter', 1)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "counter"
|
|
||||||
}, {
|
|
||||||
localContext: "counter",
|
|
||||||
target: 1 // Each update adds 1
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,62 +193,34 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
const timestamp = 1000;
|
const timestamp = 1000;
|
||||||
|
|
||||||
// Writer 1 updates name and score
|
// Writer 1 updates name and score
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(timestamp)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: timestamp,
|
.addPointer('name', 'alice')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'alice'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(timestamp + 1)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: timestamp + 1,
|
.addPointer('score', 100)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Writer 2 updates name and score concurrently
|
// Writer 2 updates name and score concurrently
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer2', 'host2')
|
||||||
creator: 'writer2',
|
.withTimestamp(timestamp + 2)
|
||||||
host: 'host2',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: timestamp + 2,
|
.addPointer('name', 'bob')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'bob'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer2', 'host2')
|
||||||
creator: 'writer2',
|
.withTimestamp(timestamp + 3)
|
||||||
host: 'host2',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: timestamp + 3,
|
.addPointer('score', 200)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 200
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
name: new LastWriteWinsPlugin(),
|
name: new LastWriteWinsPlugin(),
|
||||||
@ -365,19 +241,12 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
|
|
||||||
// Multiple writers updating different entities simultaneously
|
// Multiple writers updating different entities simultaneously
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta(`writer${i}`, `host${i}`)
|
||||||
creator: `writer${i}`,
|
.withTimestamp(timestamp)
|
||||||
host: `host${i}`,
|
.addPointer('collection', `entity${i}`, 'value')
|
||||||
timeCreated: timestamp,
|
.addPointer('value', (i + 1) * 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: `entity${i}`,
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: (i + 1) * 10 // Start from 10 to avoid 0 values
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = new LastWriteWins(lossless);
|
const resolver = new LastWriteWins(lossless);
|
||||||
@ -395,62 +264,34 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
const timestamp = 1000;
|
const timestamp = 1000;
|
||||||
|
|
||||||
// Entity1: Multiple writers competing for same property
|
// Entity1: Multiple writers competing for same property
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer1', 'host1')
|
||||||
creator: 'writer1',
|
.withTimestamp(timestamp)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'votes')
|
||||||
timeCreated: timestamp,
|
.addPointer('votes', 'option_a')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "votes"
|
|
||||||
}, {
|
|
||||||
localContext: "votes",
|
|
||||||
target: 'option_a'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer2', 'host2')
|
||||||
creator: 'writer2',
|
.withTimestamp(timestamp)
|
||||||
host: 'host2',
|
.addPointer('collection', 'entity1', 'votes')
|
||||||
timeCreated: timestamp,
|
.addPointer('votes', 'option_a')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "votes"
|
|
||||||
}, {
|
|
||||||
localContext: "votes",
|
|
||||||
target: 'option_a'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer3', 'host3')
|
||||||
creator: 'writer3',
|
.withTimestamp(timestamp)
|
||||||
host: 'host3',
|
.addPointer('collection', 'entity1', 'votes')
|
||||||
timeCreated: timestamp,
|
.addPointer('votes', 'option_b')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "votes"
|
|
||||||
}, {
|
|
||||||
localContext: "votes",
|
|
||||||
target: 'option_b'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Entity2: Single writer, no conflict
|
// Entity2: Single writer, no conflict
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('writer4', 'host4')
|
||||||
creator: 'writer4',
|
.withTimestamp(timestamp)
|
||||||
host: 'host4',
|
.addPointer('collection', 'entity2', 'status')
|
||||||
timeCreated: timestamp,
|
.addPointer('status', 'active')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity2",
|
|
||||||
targetContext: "status"
|
|
||||||
}, {
|
|
||||||
localContext: "status",
|
|
||||||
target: 'active'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
votes: new MajorityVotePlugin(),
|
votes: new MajorityVotePlugin(),
|
||||||
@ -474,19 +315,12 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
// Generate a large number of concurrent writes
|
// Generate a large number of concurrent writes
|
||||||
for (let entity = 0; entity < numEntities; entity++) {
|
for (let entity = 0; entity < numEntities; entity++) {
|
||||||
for (let writer = 0; writer < numWritersPerEntity; writer++) {
|
for (let writer = 0; writer < numWritersPerEntity; writer++) {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`)
|
||||||
creator: `writer${writer}`,
|
.withTimestamp(baseTimestamp + Math.floor(Math.random() * 1000))
|
||||||
host: `host${writer}`,
|
.addPointer('collection', `entity${entity}`, 'score')
|
||||||
timeCreated: baseTimestamp + Math.floor(Math.random() * 1000), // Random timestamps
|
.addPointer('score', Math.floor(Math.random() * 100))
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: `entity${entity}`,
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: Math.floor(Math.random() * 100) // Random scores
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,19 +344,15 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
|
|
||||||
// Add initial deltas
|
// Add initial deltas
|
||||||
for (let i = 0; i < 50; i++) {
|
for (let i = 0; i < 50; i++) {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta(
|
||||||
creator: `writer${i % 5}`,
|
`writer${i % 5}`,
|
||||||
host: `host${i % 3}`,
|
`host${i % 3}`
|
||||||
timeCreated: 1000 + i,
|
)
|
||||||
pointers: [{
|
.withTimestamp(1000 + i)
|
||||||
localContext: "collection",
|
.addPointer('collection', entityId, 'counter')
|
||||||
target: entityId,
|
.addPointer('counter', 1)
|
||||||
targetContext: "counter"
|
.buildV1()
|
||||||
}, {
|
);
|
||||||
localContext: "counter",
|
|
||||||
target: 1
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
updateCount++;
|
updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,19 +364,12 @@ describe('Concurrent Write Scenarios', () => {
|
|||||||
|
|
||||||
// Add more deltas and verify consistency
|
// Add more deltas and verify consistency
|
||||||
for (let i = 0; i < 25; i++) {
|
for (let i = 0; i < 25; i++) {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('late-writer', 'late-host')
|
||||||
creator: 'late-writer',
|
.withTimestamp(2000 + i)
|
||||||
host: 'late-host',
|
.addPointer('collection', entityId, 'counter')
|
||||||
timeCreated: 2000 + i,
|
.addPointer('counter', 2)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: entityId,
|
|
||||||
targetContext: "counter"
|
|
||||||
}, {
|
|
||||||
localContext: "counter",
|
|
||||||
target: 2
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
updateCount += 2;
|
updateCount += 2;
|
||||||
|
|
||||||
// Create a fresh resolver to avoid accumulator caching issues
|
// Create a fresh resolver to avoid accumulator caching issues
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
MinPlugin,
|
MinPlugin,
|
||||||
MaxPlugin,
|
MaxPlugin,
|
||||||
PropertyTypes,
|
PropertyTypes,
|
||||||
CollapsedDelta
|
CollapsedDelta,
|
||||||
|
createDelta
|
||||||
} from "../src";
|
} from "../src";
|
||||||
|
|
||||||
describe('Custom Resolvers', () => {
|
describe('Custom Resolvers', () => {
|
||||||
@ -25,33 +26,21 @@ describe('Custom Resolvers', () => {
|
|||||||
|
|
||||||
describe('Built-in Plugins', () => {
|
describe('Built-in Plugins', () => {
|
||||||
test('LastWriteWinsPlugin should resolve to most recent value', () => {
|
test('LastWriteWinsPlugin should resolve to most recent value', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First delta with earlier timestamp
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'name', 'first', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'first'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second delta with later timestamp (should win)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'name', 'second', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'second'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
name: new LastWriteWinsPlugin()
|
name: new LastWriteWinsPlugin()
|
||||||
@ -63,33 +52,21 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('FirstWriteWinsPlugin should resolve to earliest value', () => {
|
test('FirstWriteWinsPlugin should resolve to earliest value', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// Later delta (should be ignored by FirstWriteWins)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'name', 'second', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'second'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Earlier delta (should win with FirstWriteWins)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'name', 'first', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'first'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
name: new FirstWriteWinsPlugin()
|
name: new FirstWriteWinsPlugin()
|
||||||
@ -101,47 +78,29 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('ConcatenationPlugin should join string values chronologically', () => {
|
test('ConcatenationPlugin should join string values chronologically', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First tag
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'tags', 'red', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "tags"
|
|
||||||
}, {
|
|
||||||
localContext: "tags",
|
|
||||||
target: 'red'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second tag (with later timestamp)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 3000,
|
.withTimestamp(3000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'tags', 'blue', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "tags"
|
|
||||||
}, {
|
|
||||||
localContext: "tags",
|
|
||||||
target: 'blue'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third tag (with timestamp between first and second)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'tags', 'green', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "tags"
|
|
||||||
}, {
|
|
||||||
localContext: "tags",
|
|
||||||
target: 'green'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
tags: new ConcatenationPlugin(' ')
|
tags: new ConcatenationPlugin(' ')
|
||||||
@ -153,33 +112,21 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('ConcatenationPlugin should handle duplicates', () => {
|
test('ConcatenationPlugin should handle duplicates', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First tag
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'tags', 'red', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "tags"
|
|
||||||
}, {
|
|
||||||
localContext: "tags",
|
|
||||||
target: 'red'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Duplicate tag with later timestamp
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'tags', 'red', 'collection') // duplicate
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "tags"
|
|
||||||
}, {
|
|
||||||
localContext: "tags",
|
|
||||||
target: 'red' // duplicate
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
tags: new ConcatenationPlugin(',')
|
tags: new ConcatenationPlugin(',')
|
||||||
@ -192,76 +139,41 @@ describe('Custom Resolvers', () => {
|
|||||||
|
|
||||||
test('MajorityVotePlugin should resolve to most voted value', () => {
|
test('MajorityVotePlugin should resolve to most voted value', () => {
|
||||||
// Add 3 votes for 'red'
|
// Add 3 votes for 'red'
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity1', 'color', 'red', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "color"
|
|
||||||
}, {
|
|
||||||
localContext: "color",
|
|
||||||
target: 'red'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user2',
|
createDelta('user2', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1001,
|
.setProperty('entity1', 'color', 'red', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "color"
|
|
||||||
}, {
|
|
||||||
localContext: "color",
|
|
||||||
target: 'red'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user3',
|
createDelta('user3', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1002,
|
.setProperty('entity1', 'color', 'red', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "color"
|
|
||||||
}, {
|
|
||||||
localContext: "color",
|
|
||||||
target: 'red'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add 2 votes for 'blue'
|
// Add 2 votes for 'blue'
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user4',
|
createDelta('user4', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1003,
|
.setProperty('entity1', 'color', 'blue', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "color"
|
|
||||||
}, {
|
|
||||||
localContext: "color",
|
|
||||||
target: 'blue'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user5',
|
createDelta('user5', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1004,
|
.setProperty('entity1', 'color', 'blue', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "color"
|
|
||||||
}, {
|
|
||||||
localContext: "color",
|
|
||||||
target: 'blue'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
color: new MajorityVotePlugin()
|
color: new MajorityVotePlugin()
|
||||||
@ -273,47 +185,29 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('MinPlugin should resolve to minimum numeric value', () => {
|
test('MinPlugin should resolve to minimum numeric value', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First score (100)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 100, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second score (50) - this is the minimum
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 50, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 50
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third score (75)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 3000,
|
.withTimestamp(3000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 75, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 75
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
score: new MinPlugin()
|
score: new MinPlugin()
|
||||||
@ -325,47 +219,29 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('MaxPlugin should resolve to maximum numeric value', () => {
|
test('MaxPlugin should resolve to maximum numeric value', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First score (100)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 100, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second score (150) - this is the maximum
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 150, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 150
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third score (75)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 3000,
|
.withTimestamp(3000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 75, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 75
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
score: new MaxPlugin()
|
score: new MaxPlugin()
|
||||||
@ -380,62 +256,36 @@ describe('Custom Resolvers', () => {
|
|||||||
describe('Mixed Plugin Configurations', () => {
|
describe('Mixed Plugin Configurations', () => {
|
||||||
test('should handle different plugins for different properties', () => {
|
test('should handle different plugins for different properties', () => {
|
||||||
// Add name with different timestamps
|
// Add name with different timestamps
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity1', 'name', 'old_name', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'old_name'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Update name with newer timestamp
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'name', 'new_name', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'new_name'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add scores
|
// Add scores
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity1', 'score', 100, 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Add another score (MinPlugin will pick the smaller one)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 50, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 50
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
name: new LastWriteWinsPlugin(), // Should resolve to 'new_name'
|
name: new LastWriteWinsPlugin(), // Should resolve to 'new_name'
|
||||||
@ -450,34 +300,20 @@ describe('Custom Resolvers', () => {
|
|||||||
|
|
||||||
test('should only include entities with configured properties', () => {
|
test('should only include entities with configured properties', () => {
|
||||||
// Entity1 has configured property
|
// Entity1 has configured property
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity1', 'name', 'test', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'test'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Entity2 has non-configured property
|
// Entity2 has non-configured property
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity2', 'other_prop', 'value', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity2",
|
|
||||||
targetContext: "other"
|
|
||||||
}, {
|
|
||||||
localContext: "other",
|
|
||||||
target: 'value'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
name: new LastWriteWinsPlugin()
|
name: new LastWriteWinsPlugin()
|
||||||
@ -510,47 +346,29 @@ describe('Custom Resolvers', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// First update
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'updates', 'first', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "updates"
|
|
||||||
}, {
|
|
||||||
localContext: "updates",
|
|
||||||
target: 'first'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second update
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'updates', 'second', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "updates"
|
|
||||||
}, {
|
|
||||||
localContext: "updates",
|
|
||||||
target: 'second'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third update
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 3000,
|
.withTimestamp(3000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'updates', 'third', 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "updates"
|
|
||||||
}, {
|
|
||||||
localContext: "updates",
|
|
||||||
target: 'third'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
updates: new CountPlugin()
|
updates: new CountPlugin()
|
||||||
@ -585,47 +403,29 @@ describe('Custom Resolvers', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// First score (10)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 1000,
|
.withTimestamp(1000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 10, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second score (20)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 2000,
|
.withTimestamp(2000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 20, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Third score (30)
|
||||||
creator: 'user1',
|
lossless.ingestDelta(
|
||||||
host: 'host1',
|
createDelta('user1', 'host1')
|
||||||
timeCreated: 3000,
|
.withTimestamp(3000)
|
||||||
pointers: [{
|
.setProperty('entity1', 'score', 30, 'collection')
|
||||||
localContext: "collection",
|
.buildV1()
|
||||||
target: "entity1",
|
);
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 30
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
score: new RunningAveragePlugin()
|
score: new RunningAveragePlugin()
|
||||||
@ -650,19 +450,12 @@ describe('Custom Resolvers', () => {
|
|||||||
|
|
||||||
test('should handle non-matching property types gracefully', () => {
|
test('should handle non-matching property types gracefully', () => {
|
||||||
// Add string value to numeric plugin
|
// Add string value to numeric plugin
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(
|
||||||
creator: 'user1',
|
createDelta('user1', 'host1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
timeCreated: 1000,
|
.setProperty('entity1', 'score', 'not_a_number', 'collection')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 'not_a_number'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
score: new MinPlugin() // Expects numeric values
|
score: new MinPlugin() // Expects numeric values
|
||||||
|
187
__tests__/delta-builder.test.ts
Normal file
187
__tests__/delta-builder.test.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
|
import { DeltaV1, DeltaV2 } from '../src/core/delta';
|
||||||
|
import { Lossless } from '../src/views/lossless';
|
||||||
|
import { RhizomeNode } from '../src/node';
|
||||||
|
import { LastWriteWins } from '../src/views/resolvers/last-write-wins';
|
||||||
|
|
||||||
|
describe('DeltaBuilder', () => {
|
||||||
|
const creator = 'creator-123';
|
||||||
|
const host = 'host-456';
|
||||||
|
const node = new RhizomeNode();
|
||||||
|
|
||||||
|
describe('V1 Deltas', () => {
|
||||||
|
it('should create a basic V1 delta', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.addPointer('name', 'Test Delta', 'title')
|
||||||
|
.addPointer('description', 'A test delta', 'description')
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta).toBeInstanceOf(DeltaV1);
|
||||||
|
expect(delta.id).toBeDefined();
|
||||||
|
expect(delta.creator).toBe(creator);
|
||||||
|
expect(delta.host).toBe(host);
|
||||||
|
expect(delta.pointers).toHaveLength(2);
|
||||||
|
expect(delta.pointers[0]).toEqual({
|
||||||
|
localContext: 'name',
|
||||||
|
target: 'Test Delta',
|
||||||
|
targetContext: 'title'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a V1 delta with setProperty', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.setProperty('entity-1', 'name', 'Test Entity')
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta).toBeInstanceOf(DeltaV1);
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'entity',
|
||||||
|
target: 'entity-1',
|
||||||
|
targetContext: 'name'
|
||||||
|
});
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'name',
|
||||||
|
target: 'Test Entity',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the entity property resolves correctly
|
||||||
|
const lossless = new Lossless(node);
|
||||||
|
lossless.ingestDelta(delta);
|
||||||
|
const lossy = new LastWriteWins(lossless);
|
||||||
|
const result = lossy.resolve();
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!['entity-1'].properties.name).toBe('Test Entity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a V1 delta with setProperty and entityLabel', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.setProperty('entity-1', 'name', 'Test Entity', 'user')
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta).toBeInstanceOf(DeltaV1);
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'user',
|
||||||
|
target: 'entity-1',
|
||||||
|
targetContext: 'name'
|
||||||
|
});
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'name',
|
||||||
|
target: 'Test Entity',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the entity property resolves correctly
|
||||||
|
const lossless = new Lossless(node);
|
||||||
|
lossless.ingestDelta(delta);
|
||||||
|
const lossy = new LastWriteWins(lossless);
|
||||||
|
const result = lossy.resolve();
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result!['entity-1'].properties.name).toBe('Test Entity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a V1 delta with relationships', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.relate('user-1', 'follows', 'user-2')
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'follows',
|
||||||
|
target: 'user-2',
|
||||||
|
targetContext: 'follows'
|
||||||
|
});
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: 'source',
|
||||||
|
target: 'user-1',
|
||||||
|
targetContext: 'follows'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('V2 Deltas', () => {
|
||||||
|
it('should create a basic V2 delta', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.addPointer('name', 'Test Delta V2', 'title')
|
||||||
|
.buildV2();
|
||||||
|
|
||||||
|
expect(delta).toBeInstanceOf(DeltaV2);
|
||||||
|
expect(delta.id).toBeDefined();
|
||||||
|
expect(delta.creator).toBe(creator);
|
||||||
|
expect(delta.host).toBe(host);
|
||||||
|
expect(delta.pointers).toHaveProperty('name');
|
||||||
|
expect(delta.pointers.name).toEqual({ 'Test Delta V2': 'title' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a V2 delta with setProperty', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.setProperty('entity-1', 'name', 'Test Entity')
|
||||||
|
.buildV2();
|
||||||
|
|
||||||
|
expect(delta.pointers).toHaveProperty('name', 'Test Entity');
|
||||||
|
expect(delta.pointers).toHaveProperty('entity', { 'entity-1': 'name' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a V2 delta with relationships', () => {
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.relate('user-1', 'follows', 'user-2')
|
||||||
|
.buildV2();
|
||||||
|
|
||||||
|
expect(delta.pointers).toHaveProperty('follows', { 'user-2': 'follows' });
|
||||||
|
expect(delta.pointers).toHaveProperty('source', { 'user-1': 'follows' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Common functionality', () => {
|
||||||
|
it('should support custom IDs', () => {
|
||||||
|
const customId = 'custom-delta-id';
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.withId(customId)
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta.id).toBe(customId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support transactions', () => {
|
||||||
|
const txId = 'tx-123';
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.inTransaction(txId)
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
// Check for transaction ID in pointers
|
||||||
|
expect(delta.pointers).toContainEqual({
|
||||||
|
localContext: '_transaction',
|
||||||
|
target: txId,
|
||||||
|
targetContext: 'deltas'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support transactions in V2', () => {
|
||||||
|
const txId = 'tx-123';
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.inTransaction(txId)
|
||||||
|
.buildV2();
|
||||||
|
|
||||||
|
// Check for transaction ID in V2 pointers
|
||||||
|
expect(delta.pointers['_transaction']).toEqual({ [txId]: 'deltas' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support negation', () => {
|
||||||
|
const negatedId = 'delta-to-negate';
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.negate(negatedId)
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
// Check for negation in pointers
|
||||||
|
const negationPointer = delta.pointers.find(p => p.localContext === '_negates');
|
||||||
|
expect(negationPointer).toBeDefined();
|
||||||
|
expect(negationPointer?.target).toBe(negatedId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support custom timestamps', () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const delta = createDelta(creator, host)
|
||||||
|
.withTimestamp(timestamp)
|
||||||
|
.buildV1();
|
||||||
|
|
||||||
|
expect(delta.timeCreated).toBe(timestamp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,19 +1,12 @@
|
|||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import {DeltaV1, DeltaV2} from "../src";
|
import {DeltaV1, DeltaV2} from "../src";
|
||||||
|
|
||||||
describe("Delta", () => {
|
describe("Delta", () => {
|
||||||
it("can convert DeltaV1 to DeltaV2", () => {
|
it("can convert DeltaV1 to DeltaV2", () => {
|
||||||
const deltaV1 = new DeltaV1({
|
const deltaV1 = createDelta('a', 'h')
|
||||||
creator: 'a',
|
.addPointer('color', 'red')
|
||||||
host: 'h',
|
.addPointer('furniture', 'chair-1', 'color')
|
||||||
pointers: [{
|
.buildV1();
|
||||||
localContext: 'color',
|
|
||||||
target: 'red'
|
|
||||||
}, {
|
|
||||||
localContext: 'furniture',
|
|
||||||
target: 'chair-1',
|
|
||||||
targetContext: 'color'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const deltaV2 = DeltaV2.fromV1(deltaV1);
|
const deltaV2 = DeltaV2.fromV1(deltaV1);
|
||||||
|
|
||||||
@ -27,14 +20,10 @@ describe("Delta", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can convert DeltaV2 to DeltaV1", () => {
|
it("can convert DeltaV2 to DeltaV1", () => {
|
||||||
const deltaV2 = new DeltaV2({
|
const deltaV2 = createDelta('a', 'h')
|
||||||
creator: 'a',
|
.addPointer('color', 'red')
|
||||||
host: 'h',
|
.addPointer('furniture', 'chair-1', 'color')
|
||||||
pointers: {
|
.buildV2();
|
||||||
color: 'red',
|
|
||||||
furniture: {'chair-1': 'color'}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const deltaV1 = deltaV2.toV1();
|
const deltaV1 = deltaV2.toV1();
|
||||||
|
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
// Set up environment variables for tests
|
// Set up environment variables for tests
|
||||||
process.env.DEBUG = 'rz:*';
|
// DEBUG handling examples:
|
||||||
|
// npm test // will set DEBUG=rz:* by default
|
||||||
|
// NO_DEBUG=true npm test // will not set DEBUG
|
||||||
|
// DEBUG=other npm test // will set DEBUG=other
|
||||||
|
if (!process.env.DEBUG && !process.env.NO_DEBUG) {
|
||||||
|
process.env.DEBUG = 'rz:*';
|
||||||
|
}
|
||||||
|
|
||||||
// Extend the global Jest namespace
|
// Extend the global Jest namespace
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import {Delta, LastWriteWins, Lossless, RhizomeNode} from "../src";
|
import {Delta, LastWriteWins, Lossless, RhizomeNode} from "../src";
|
||||||
const debug = Debug('test:last-write-wins');
|
const debug = Debug('test:last-write-wins');
|
||||||
|
|
||||||
@ -11,31 +12,15 @@ describe('Last write wins', () => {
|
|||||||
const lossy = new LastWriteWins(lossless);
|
const lossy = new LastWriteWins(lossless);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('a', 'h')
|
||||||
creator: 'a',
|
.setProperty('broccoli', 'want', 95, 'vegetable')
|
||||||
host: 'h',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "vegetable",
|
|
||||||
target: "broccoli",
|
|
||||||
targetContext: "want"
|
|
||||||
}, {
|
|
||||||
localContext: "desire",
|
|
||||||
target: 95,
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('a', 'h')
|
||||||
creator: 'a',
|
.setProperty('broccoli', 'want', 90, 'vegetable')
|
||||||
host: 'h',
|
.buildV1()
|
||||||
pointers: [{
|
);
|
||||||
localContext: "vegetable",
|
|
||||||
target: "broccoli",
|
|
||||||
targetContext: "want"
|
|
||||||
}, {
|
|
||||||
localContext: "want",
|
|
||||||
target: 90,
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('our resolver should return the most recently written value', () => {
|
it('our resolver should return the most recently written value', () => {
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
import {Delta, DeltaFilter, DeltaV2} from '../src/core';
|
import {Delta, DeltaFilter, DeltaV2} from '../src/core';
|
||||||
import {Lossless} from '../src/views';
|
import {Lossless} from '../src/views';
|
||||||
import {RhizomeNode} from '../src/node';
|
import {RhizomeNode} from '../src/node';
|
||||||
|
import {createDelta} from '../src/core/delta-builder';
|
||||||
|
|
||||||
describe('Lossless', () => {
|
describe('Lossless', () => {
|
||||||
const node = new RhizomeNode();
|
const node = new RhizomeNode();
|
||||||
|
|
||||||
it('creates a lossless view of keanu as neo in the matrix', () => {
|
it('creates a lossless view of keanu as neo in the matrix', () => {
|
||||||
const delta = new DeltaV2({
|
const delta = createDelta('a', 'h')
|
||||||
creator: 'a',
|
.addPointer('actor', 'keanu', 'roles')
|
||||||
host: 'h',
|
.addPointer('role', 'neo', 'actor')
|
||||||
pointers: {
|
.addPointer('film', 'the_matrix', 'cast')
|
||||||
actor: {"keanu": "roles"},
|
.addPointer('base_salary', 1000000)
|
||||||
role: {"neo": "actor"},
|
.addPointer('salary_currency', 'usd')
|
||||||
film: {"the_matrix": "cast"},
|
.buildV1();
|
||||||
base_salary: 1000000,
|
|
||||||
salary_currency: "usd"
|
|
||||||
}
|
|
||||||
}).toV1();
|
|
||||||
|
|
||||||
expect(delta.pointers).toMatchObject([{
|
expect(delta.pointers).toMatchObject([{
|
||||||
localContext: "actor",
|
localContext: "actor",
|
||||||
@ -94,30 +91,89 @@ describe('Lossless', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts DeltaV2 instances', () => {
|
||||||
|
const delta = createDelta('a', 'h')
|
||||||
|
.addPointer('actor', 'keanu', 'roles')
|
||||||
|
.addPointer('role', 'neo', 'actor')
|
||||||
|
.addPointer('film', 'the_matrix', 'cast')
|
||||||
|
.addPointer('base_salary', 1000000)
|
||||||
|
.addPointer('salary_currency', 'usd')
|
||||||
|
.buildV2();
|
||||||
|
|
||||||
|
const lossless = new Lossless(node);
|
||||||
|
|
||||||
|
lossless.ingestDelta(delta);
|
||||||
|
|
||||||
|
expect(lossless.view()).toMatchObject({
|
||||||
|
keanu: {
|
||||||
|
referencedAs: ["actor"],
|
||||||
|
propertyDeltas: {
|
||||||
|
roles: [{
|
||||||
|
creator: "a",
|
||||||
|
host: "h",
|
||||||
|
pointers: [
|
||||||
|
{actor: "keanu"},
|
||||||
|
{role: "neo"},
|
||||||
|
{film: "the_matrix"},
|
||||||
|
{base_salary: 1000000},
|
||||||
|
{salary_currency: "usd"},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
neo: {
|
||||||
|
referencedAs: ["role"],
|
||||||
|
propertyDeltas: {
|
||||||
|
actor: [{
|
||||||
|
creator: "a",
|
||||||
|
host: "h",
|
||||||
|
pointers: [
|
||||||
|
{actor: "keanu"},
|
||||||
|
{role: "neo"},
|
||||||
|
{film: "the_matrix"},
|
||||||
|
{base_salary: 1000000},
|
||||||
|
{salary_currency: "usd"},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
the_matrix: {
|
||||||
|
referencedAs: ["film"],
|
||||||
|
propertyDeltas: {
|
||||||
|
cast: [{
|
||||||
|
creator: "a",
|
||||||
|
host: "h",
|
||||||
|
pointers: [
|
||||||
|
{actor: "keanu"},
|
||||||
|
{role: "neo"},
|
||||||
|
{film: "the_matrix"},
|
||||||
|
{base_salary: 1000000},
|
||||||
|
{salary_currency: "usd"},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('can filter deltas', () => {
|
describe('can filter deltas', () => {
|
||||||
const lossless = new Lossless(node);
|
const lossless = new Lossless(node);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
lossless.ingestDelta(new Delta({
|
// First delta
|
||||||
creator: 'A',
|
lossless.ingestDelta(
|
||||||
host: 'H',
|
createDelta('A', 'H')
|
||||||
pointers: [{
|
.addPointer('1', 'ace', 'value')
|
||||||
localContext: "1",
|
.buildV1()
|
||||||
target: "ace",
|
);
|
||||||
targetContext: "value"
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
// Second delta
|
||||||
creator: 'B',
|
lossless.ingestDelta(
|
||||||
host: 'H',
|
createDelta('B', 'H')
|
||||||
pointers: [{
|
|
||||||
// 10 11j 12q 13k 14a
|
// 10 11j 12q 13k 14a
|
||||||
localContext: "14",
|
.addPointer('14', 'ace', 'value')
|
||||||
target: "ace",
|
.buildV1()
|
||||||
targetContext: "value"
|
);
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(lossless.view()).toMatchObject({
|
expect(lossless.view()).toMatchObject({
|
||||||
ace: {
|
ace: {
|
||||||
@ -182,51 +238,40 @@ describe('Lossless', () => {
|
|||||||
const transactionId = 'tx-filter-test';
|
const transactionId = 'tx-filter-test';
|
||||||
|
|
||||||
// Declare transaction with 3 deltas
|
// Declare transaction with 3 deltas
|
||||||
losslessT.ingestDelta(new Delta({
|
losslessT.ingestDelta(
|
||||||
creator: 'system',
|
createDelta('system', 'H')
|
||||||
host: 'H',
|
.declareTransaction(transactionId, 3)
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
);
|
||||||
{ localContext: 'size', target: 3 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// A1: First delta from creator A
|
// A1: First delta from creator A
|
||||||
losslessT.ingestDelta(new Delta({
|
losslessT.ingestDelta(
|
||||||
creator: 'A',
|
createDelta('A', 'H')
|
||||||
host: 'H',
|
.inTransaction(transactionId)
|
||||||
pointers: [
|
.setProperty('process1', 'status', 'started', 'step')
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.buildV1()
|
||||||
{ localContext: 'step', target: 'process1', targetContext: 'status' },
|
);
|
||||||
{ localContext: 'value', target: 'started' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// B: Delta from creator B
|
// B: Delta from creator B
|
||||||
losslessT.ingestDelta(new Delta({
|
losslessT.ingestDelta(
|
||||||
creator: 'B',
|
createDelta('B', 'H')
|
||||||
host: 'H',
|
.inTransaction(transactionId)
|
||||||
pointers: [
|
.setProperty('process1', 'status', 'processing', 'step')
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.buildV1()
|
||||||
{ localContext: 'step', target: 'process1', targetContext: 'status' },
|
);
|
||||||
{ localContext: 'value', target: 'processing' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Transaction incomplete - nothing should show
|
// Transaction incomplete - nothing should show
|
||||||
const incompleteView = losslessT.view(['process1']);
|
const incompleteView = losslessT.view(['process1']);
|
||||||
expect(incompleteView.process1).toBeUndefined();
|
expect(incompleteView.process1).toBeUndefined();
|
||||||
|
|
||||||
// A2: Second delta from creator A completes transaction
|
// A2: Second delta from creator A completes transaction
|
||||||
losslessT.ingestDelta(new Delta({
|
losslessT.ingestDelta(
|
||||||
creator: 'A',
|
createDelta('A', 'H')
|
||||||
host: 'H',
|
.inTransaction(transactionId)
|
||||||
pointers: [
|
.addPointer('step', 'process1', 'status')
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.addPointer('value', 'completed')
|
||||||
{ localContext: 'step', target: 'process1', targetContext: 'status' },
|
.buildV1()
|
||||||
{ localContext: 'value', target: 'completed' }
|
);
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// All deltas visible now
|
// All deltas visible now
|
||||||
const completeView = losslessT.view(['process1']);
|
const completeView = losslessT.view(['process1']);
|
||||||
@ -242,4 +287,5 @@ describe('Lossless', () => {
|
|||||||
expect(filteredView.process1.propertyDeltas.status.every(d => d.creator === 'A')).toBe(true);
|
expect(filteredView.process1.propertyDeltas.status.every(d => d.creator === 'A')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
import {
|
import {
|
||||||
Delta,
|
|
||||||
PointerTarget,
|
PointerTarget,
|
||||||
lastValueFromDeltas,
|
lastValueFromDeltas,
|
||||||
valueFromCollapsedDelta,
|
valueFromCollapsedDelta,
|
||||||
@ -9,6 +8,7 @@ import {
|
|||||||
Lossy,
|
Lossy,
|
||||||
RhizomeNode
|
RhizomeNode
|
||||||
} from "../src";
|
} from "../src";
|
||||||
|
import { createDelta } from "../src/core/delta-builder";
|
||||||
const debug = Debug('test:lossy');
|
const debug = Debug('test:lossy');
|
||||||
|
|
||||||
type Role = {
|
type Role = {
|
||||||
@ -62,29 +62,14 @@ describe('Lossy', () => {
|
|||||||
const lossy = new Summarizer(lossless);
|
const lossy = new Summarizer(lossless);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('a', 'h')
|
||||||
creator: 'a',
|
.addPointer('actor', 'keanu', 'roles')
|
||||||
host: 'h',
|
.addPointer('role', 'neo', 'actor')
|
||||||
pointers: [{
|
.addPointer('film', 'the_matrix', 'cast')
|
||||||
localContext: "actor",
|
.addPointer('base_salary', 1000000)
|
||||||
target: "keanu",
|
.addPointer('salary_currency', 'usd')
|
||||||
targetContext: "roles"
|
.buildV1()
|
||||||
}, {
|
);
|
||||||
localContext: "role",
|
|
||||||
target: "neo",
|
|
||||||
targetContext: "actor"
|
|
||||||
}, {
|
|
||||||
localContext: "film",
|
|
||||||
target: "the_matrix",
|
|
||||||
targetContext: "cast"
|
|
||||||
}, {
|
|
||||||
localContext: "base_salary",
|
|
||||||
target: 1000000
|
|
||||||
}, {
|
|
||||||
localContext: "salary_currency",
|
|
||||||
target: "usd"
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('example summary', () => {
|
it('example summary', () => {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import { DefaultSchemaRegistry } from '../src/schema';
|
import { DefaultSchemaRegistry } from '../src/schema';
|
||||||
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema';
|
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema';
|
||||||
import { TypedCollectionImpl } from '../src/collections';
|
import { TypedCollectionImpl } from '../src/collections';
|
||||||
@ -76,17 +76,13 @@ describe('Multi-Pointer Delta Resolution', () => {
|
|||||||
await roleCollection.put('neo', { name: 'Neo' });
|
await roleCollection.put('neo', { name: 'Neo' });
|
||||||
|
|
||||||
// Create a complex casting delta with multiple entity references and scalar values
|
// Create a complex casting delta with multiple entity references and scalar values
|
||||||
const castingDelta = new Delta({
|
const castingDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('actors', 'keanu', 'filmography')
|
||||||
host: node.config.peerId,
|
.addPointer('movies', 'matrix', 'cast')
|
||||||
pointers: [
|
.addPointer('roles', 'neo', 'portrayals')
|
||||||
{ localContext: 'actors', target: 'keanu', targetContext: 'filmography' },
|
.addPointer('salary', 15000000)
|
||||||
{ localContext: 'movies', target: 'matrix', targetContext: 'cast' },
|
.addPointer('contract_date', '1999-03-31')
|
||||||
{ localContext: 'roles', target: 'neo', targetContext: 'portrayals' },
|
.buildV1();
|
||||||
{ localContext: 'salary', target: 15000000 },
|
|
||||||
{ localContext: 'contract_date', target: '1999-03-31' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(castingDelta);
|
node.lossless.ingestDelta(castingDelta);
|
||||||
|
|
||||||
// Test from Keanu's perspective
|
// Test from Keanu's perspective
|
||||||
@ -164,17 +160,13 @@ describe('Multi-Pointer Delta Resolution', () => {
|
|||||||
await personCollection.put('bob', { name: 'Bob' });
|
await personCollection.put('bob', { name: 'Bob' });
|
||||||
|
|
||||||
// Create a relationship delta with one entity reference and multiple scalars
|
// Create a relationship delta with one entity reference and multiple scalars
|
||||||
const relationshipDelta = new Delta({
|
const relationshipDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('people', 'alice', 'relationships')
|
||||||
host: node.config.peerId,
|
.addPointer('partner', 'bob')
|
||||||
pointers: [
|
.addPointer('type', 'friendship')
|
||||||
{ localContext: 'people', target: 'alice', targetContext: 'relationships' },
|
.addPointer('since', '2020-01-15')
|
||||||
{ localContext: 'partner', target: 'bob' }, // Entity reference
|
.addPointer('intensity', 8)
|
||||||
{ localContext: 'type', target: 'friendship' }, // Scalar
|
.buildV1();
|
||||||
{ localContext: 'since', target: '2020-01-15' }, // Scalar
|
|
||||||
{ localContext: 'intensity', target: 8 } // Scalar number
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(relationshipDelta);
|
node.lossless.ingestDelta(relationshipDelta);
|
||||||
|
|
||||||
// Test from Alice's perspective
|
// Test from Alice's perspective
|
||||||
@ -243,17 +235,13 @@ describe('Multi-Pointer Delta Resolution', () => {
|
|||||||
await designerCollection.put('bob', { name: 'Bob Designer' });
|
await designerCollection.put('bob', { name: 'Bob Designer' });
|
||||||
|
|
||||||
// Create a collaboration delta with multiple entity references
|
// Create a collaboration delta with multiple entity references
|
||||||
const collaborationDelta = new Delta({
|
const collaborationDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('projects', 'website', 'collaborations')
|
||||||
host: node.config.peerId,
|
.addPointer('developer', 'alice')
|
||||||
pointers: [
|
.addPointer('designer', 'bob')
|
||||||
{ localContext: 'projects', target: 'website', targetContext: 'collaborations' },
|
.addPointer('budget', 50000)
|
||||||
{ localContext: 'developer', target: 'alice' }, // Entity reference
|
.addPointer('deadline', '2024-06-01')
|
||||||
{ localContext: 'designer', target: 'bob' }, // Entity reference
|
.buildV1();
|
||||||
{ localContext: 'budget', target: 50000 }, // Scalar
|
|
||||||
{ localContext: 'deadline', target: '2024-06-01' } // Scalar
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(collaborationDelta);
|
node.lossless.ingestDelta(collaborationDelta);
|
||||||
|
|
||||||
// Test from project's perspective
|
// Test from project's perspective
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import { NegationHelper } from '../src/features';
|
import { NegationHelper } from '../src/features';
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Lossless } from '../src/views';
|
import { Lossless } from '../src/views';
|
||||||
@ -17,14 +17,9 @@ describe('Negation System', () => {
|
|||||||
|
|
||||||
describe('Negation Helper', () => {
|
describe('Negation Helper', () => {
|
||||||
it('should create negation deltas correctly', () => {
|
it('should create negation deltas correctly', () => {
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Alice')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'name', target: 'entity1', targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: 'Alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negationDelta = NegationHelper.createNegation(
|
const negationDelta = NegationHelper.createNegation(
|
||||||
originalDelta.id,
|
originalDelta.id,
|
||||||
@ -32,23 +27,20 @@ describe('Negation System', () => {
|
|||||||
'host1'
|
'host1'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(negationDelta.isNegation).toBe(true);
|
|
||||||
expect(negationDelta.negatedDeltaId).toBe(originalDelta.id);
|
|
||||||
expect(negationDelta.creator).toBe('moderator');
|
expect(negationDelta.creator).toBe('moderator');
|
||||||
expect(negationDelta.pointers).toHaveLength(1);
|
expect(negationDelta.pointers).toHaveLength(1);
|
||||||
expect(negationDelta.pointers[0]).toEqual({
|
expect(negationDelta.pointers[0]).toEqual({
|
||||||
localContext: 'negates',
|
localContext: '_negates',
|
||||||
target: originalDelta.id,
|
target: originalDelta.id,
|
||||||
targetContext: 'negated_by'
|
targetContext: 'negated_by'
|
||||||
});
|
});
|
||||||
|
expect(NegationHelper.isNegationDelta(negationDelta)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should identify negation deltas', () => {
|
it('should identify negation deltas', () => {
|
||||||
const regularDelta = new Delta({
|
const regularDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negationDelta = NegationHelper.createNegation(
|
const negationDelta = NegationHelper.createNegation(
|
||||||
'delta-to-negate',
|
'delta-to-negate',
|
||||||
@ -71,27 +63,21 @@ describe('Negation System', () => {
|
|||||||
const extractedId = NegationHelper.getNegatedDeltaId(negationDelta);
|
const extractedId = NegationHelper.getNegatedDeltaId(negationDelta);
|
||||||
expect(extractedId).toBe(targetDeltaId);
|
expect(extractedId).toBe(targetDeltaId);
|
||||||
|
|
||||||
const regularDelta = new Delta({
|
const regularDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull();
|
expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find negations for specific deltas', () => {
|
it('should find negations for specific deltas', () => {
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('entity1', 'age', 25)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1');
|
const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1');
|
||||||
@ -110,17 +96,13 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should check if deltas are negated', () => {
|
it('should check if deltas are negated', () => {
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('entity1', 'age', 25)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
const allDeltas = [delta1, delta2, negation1];
|
const allDeltas = [delta1, delta2, negation1];
|
||||||
@ -130,23 +112,17 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out negated deltas', () => {
|
it('should filter out negated deltas', () => {
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('entity1', 'age', 25)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta3 = new Delta({
|
const delta3 = createDelta('user3', 'host1')
|
||||||
creator: 'user3',
|
.setProperty('entity1', 'email', 'entity1@example.com')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'email', target: 'entity1', targetContext: 'email' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1');
|
const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1');
|
||||||
@ -160,17 +136,13 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should provide negation statistics', () => {
|
it('should provide negation statistics', () => {
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity1', 'name', 'Entity 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('entity1', 'age', 25)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
const allDeltas = [delta1, delta2, negation1];
|
const allDeltas = [delta1, delta2, negation1];
|
||||||
@ -189,22 +161,18 @@ describe('Negation System', () => {
|
|||||||
const baseTime = Date.now();
|
const baseTime = Date.now();
|
||||||
|
|
||||||
// Create deltas with specific timestamps
|
// Create deltas with specific timestamps
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(baseTime)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'status', 'active')
|
||||||
timeCreated: baseTime,
|
.buildV1();
|
||||||
pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
negation1.timeCreated = baseTime + 1000; // 1 second later
|
negation1.timeCreated = baseTime + 1000; // 1 second later
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(baseTime + 2000)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'status', 'inactive')
|
||||||
timeCreated: baseTime + 2000, // 2 seconds later
|
.buildV1();
|
||||||
pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1');
|
const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1');
|
||||||
negation2.timeCreated = baseTime + 3000; // 3 seconds later
|
negation2.timeCreated = baseTime + 3000; // 3 seconds later
|
||||||
@ -220,14 +188,9 @@ describe('Negation System', () => {
|
|||||||
describe('Lossless View Integration', () => {
|
describe('Lossless View Integration', () => {
|
||||||
it('should filter negated deltas in lossless views', () => {
|
it('should filter negated deltas in lossless views', () => {
|
||||||
// Create original delta
|
// Create original delta
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('user123', 'name', 'Alice')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'name', target: 'user123', targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: 'Alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create negation delta
|
// Create negation delta
|
||||||
const negationDelta = NegationHelper.createNegation(
|
const negationDelta = NegationHelper.createNegation(
|
||||||
@ -238,14 +201,9 @@ describe('Negation System', () => {
|
|||||||
|
|
||||||
|
|
||||||
// Create another non-negated delta
|
// Create another non-negated delta
|
||||||
const nonNegatedDelta = new Delta({
|
const nonNegatedDelta = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('user123', 'age', 25)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'age', target: 'user123', targetContext: 'age' },
|
|
||||||
{ localContext: 'value', target: 25 }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ingest all deltas
|
// Ingest all deltas
|
||||||
lossless.ingestDelta(originalDelta);
|
lossless.ingestDelta(originalDelta);
|
||||||
@ -263,14 +221,9 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple negations of the same delta', () => {
|
it('should handle multiple negations of the same delta', () => {
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('post1', 'content', 'Original content')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'content', target: 'post1', targetContext: 'content' },
|
|
||||||
{ localContext: 'value', target: 'Original content' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1');
|
||||||
const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1');
|
const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1');
|
||||||
@ -286,23 +239,13 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should provide negation statistics for entities', () => {
|
it('should provide negation statistics for entities', () => {
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('article1', 'title', 'Original Title')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'title', target: 'article1', targetContext: 'title' },
|
|
||||||
{ localContext: 'value', target: 'Original Title' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('article1', 'content', 'Article content')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'content', target: 'article1', targetContext: 'content' },
|
|
||||||
{ localContext: 'value', target: 'Article content' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
|
||||||
|
|
||||||
@ -321,14 +264,9 @@ describe('Negation System', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should retrieve negation deltas for entities', () => {
|
it('should retrieve negation deltas for entities', () => {
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('task1', 'status', 'pending')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'status', target: 'task1', targetContext: 'status' },
|
|
||||||
{ localContext: 'value', target: 'pending' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negationDelta = NegationHelper.createNegation(
|
const negationDelta = NegationHelper.createNegation(
|
||||||
originalDelta.id,
|
originalDelta.id,
|
||||||
@ -349,25 +287,16 @@ describe('Negation System', () => {
|
|||||||
const transactionId = 'tx-negation';
|
const transactionId = 'tx-negation';
|
||||||
|
|
||||||
// Create transaction declaration
|
// Create transaction declaration
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Create original delta in transaction
|
// Create original delta in transaction
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.setProperty('post1', 'comments', 'Inappropriate comment')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'comment', target: 'post1', targetContext: 'comments' },
|
|
||||||
{ localContext: 'text', target: 'Inappropriate comment' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create negation delta in same transaction
|
// Create negation delta in same transaction
|
||||||
const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1');
|
const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1');
|
||||||
@ -389,30 +318,20 @@ describe('Negation System', () => {
|
|||||||
const baseTime = Date.now();
|
const baseTime = Date.now();
|
||||||
|
|
||||||
// User posts content
|
// User posts content
|
||||||
const postDelta = new Delta({
|
const postDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(baseTime)
|
||||||
host: 'host1',
|
.setProperty('post1', 'content', 'Original post')
|
||||||
timeCreated: baseTime,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'content', target: 'post1', targetContext: 'content' },
|
|
||||||
{ localContext: 'value', target: 'Original post' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Moderator negates it
|
// Moderator negates it
|
||||||
const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1');
|
const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1');
|
||||||
negationDelta.timeCreated = baseTime + 1000;
|
negationDelta.timeCreated = baseTime + 1000;
|
||||||
|
|
||||||
// User edits content (after negation)
|
// User edits content (after negation)
|
||||||
const editDelta = new Delta({
|
const editDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(baseTime + 2000)
|
||||||
host: 'host1',
|
.setProperty('post1', 'content', 'Edited post')
|
||||||
timeCreated: baseTime + 2000,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'content', target: 'post1', targetContext: 'content' },
|
|
||||||
{ localContext: 'value', target: 'Edited post' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
lossless.ingestDelta(postDelta);
|
lossless.ingestDelta(postDelta);
|
||||||
lossless.ingestDelta(negationDelta);
|
lossless.ingestDelta(negationDelta);
|
||||||
@ -447,14 +366,10 @@ describe('Negation System', () => {
|
|||||||
|
|
||||||
it('should handle self-referential entities in negations', () => {
|
it('should handle self-referential entities in negations', () => {
|
||||||
// Create a delta that references itself
|
// Create a delta that references itself
|
||||||
const selfRefDelta = new Delta({
|
const selfRefDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('node1', 'parent', 'node1')
|
||||||
host: 'host1',
|
.setProperty('node1', 'child', 'node1') // Self-reference
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'parent', target: 'node1', targetContext: 'parent' },
|
|
||||||
{ localContext: 'child', target: 'node1' } // Self-reference
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1');
|
const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1');
|
||||||
|
|
||||||
@ -470,14 +385,9 @@ describe('Negation System', () => {
|
|||||||
const testLossless = new Lossless(testNode);
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
// Create the original delta
|
// Create the original delta
|
||||||
const originalDelta = new Delta({
|
const originalDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity2', 'title', 'Draft')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'title', target: 'entity2', targetContext: 'title' },
|
|
||||||
{ localContext: 'status', target: 'Draft' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create two negations of the same delta
|
// Create two negations of the same delta
|
||||||
const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1');
|
const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1');
|
||||||
@ -506,14 +416,9 @@ describe('Negation System', () => {
|
|||||||
const testLossless = new Lossless(testNode);
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
// Create the original delta
|
// Create the original delta
|
||||||
const deltaA = new Delta({
|
const deltaA = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity3', 'content', 'Hello World')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'content', target: 'entity3', targetContext: 'content' },
|
|
||||||
{ localContext: 'text', target: 'Hello World' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a chain of negations: B negates A, C negates B, D negates C
|
// Create a chain of negations: B negates A, C negates B, D negates C
|
||||||
const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1');
|
const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1');
|
||||||
@ -584,23 +489,13 @@ describe('Negation System', () => {
|
|||||||
const testLossless = new Lossless(testNode);
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
// Create two independent deltas
|
// Create two independent deltas
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.setProperty('entity4', 'item', 'Item 1')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
|
|
||||||
{ localContext: 'name', target: 'Item 1' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user2', 'host1')
|
||||||
creator: 'user2',
|
.setProperty('entity4', 'item', 'Item 2')
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
|
|
||||||
{ localContext: 'name', target: 'Item 2' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create negations for both deltas
|
// Create negations for both deltas
|
||||||
const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1');
|
const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1');
|
||||||
|
@ -12,7 +12,7 @@ import Debug from 'debug';
|
|||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
|
|
||||||
const debug = Debug('rz:test:nested-resolution-performance');
|
const debug = Debug('rz:test:nested-resolution-performance');
|
||||||
import { Delta } from '../src/core';
|
import { Delta, createDelta } from '../src/core';
|
||||||
import { DefaultSchemaRegistry } from '../src/schema';
|
import { DefaultSchemaRegistry } from '../src/schema';
|
||||||
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema';
|
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema';
|
||||||
import { TypedCollectionImpl } from '../src/collections';
|
import { TypedCollectionImpl } from '../src/collections';
|
||||||
@ -80,14 +80,9 @@ describe('Nested Object Resolution Performance', () => {
|
|||||||
const friendIndex = Math.floor(Math.random() * userCount);
|
const friendIndex = Math.floor(Math.random() * userCount);
|
||||||
if (friendIndex !== i) {
|
if (friendIndex !== i) {
|
||||||
const friendId = userIds[friendIndex];
|
const friendId = userIds[friendIndex];
|
||||||
const friendshipDelta = new Delta({
|
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.setProperty(userId, 'friends', friendId, 'users')
|
||||||
host: node.config.peerId,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: userId, targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: friendId }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendshipDelta);
|
node.lossless.ingestDelta(friendshipDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,14 +93,9 @@ describe('Nested Object Resolution Performance', () => {
|
|||||||
const followerIndex = Math.floor(Math.random() * userCount);
|
const followerIndex = Math.floor(Math.random() * userCount);
|
||||||
if (followerIndex !== i) {
|
if (followerIndex !== i) {
|
||||||
const followerId = userIds[followerIndex];
|
const followerId = userIds[followerIndex];
|
||||||
const followDelta = new Delta({
|
const followDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.setProperty(userId, 'followers', followerId, 'users')
|
||||||
host: node.config.peerId,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: userId, targetContext: 'followers' },
|
|
||||||
{ localContext: 'followers', target: followerId }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(followDelta);
|
node.lossless.ingestDelta(followDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,14 +104,9 @@ describe('Nested Object Resolution Performance', () => {
|
|||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure
|
const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure
|
||||||
const mentorId = userIds[mentorIndex];
|
const mentorId = userIds[mentorIndex];
|
||||||
const mentorshipDelta = new Delta({
|
const mentorshipDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.setProperty(userId, 'mentor', mentorId, 'users')
|
||||||
host: node.config.peerId,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: userId, targetContext: 'mentor' },
|
|
||||||
{ localContext: 'mentor', target: mentorId }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(mentorshipDelta);
|
node.lossless.ingestDelta(mentorshipDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,14 +194,9 @@ describe('Nested Object Resolution Performance', () => {
|
|||||||
const currentId = userIds[i];
|
const currentId = userIds[i];
|
||||||
const nextId = userIds[i + 1];
|
const nextId = userIds[i + 1];
|
||||||
|
|
||||||
const linkDelta = new Delta({
|
const linkDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.setProperty(currentId, 'next', nextId, 'users')
|
||||||
host: node.config.peerId,
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'users', target: currentId, targetContext: 'next' },
|
|
||||||
{ localContext: 'next', target: nextId }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(linkDelta);
|
node.lossless.ingestDelta(linkDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,14 +288,10 @@ describe('Nested Object Resolution Performance', () => {
|
|||||||
const connectedIndex = (i + j) % userCount;
|
const connectedIndex = (i + j) % userCount;
|
||||||
const connectedId = userIds[connectedIndex];
|
const connectedId = userIds[connectedIndex];
|
||||||
|
|
||||||
const connectionDelta = new Delta({
|
const connectionDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', userId, 'connections')
|
||||||
host: node.config.peerId,
|
.addPointer('connections', connectedId)
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: userId, targetContext: 'connections' },
|
|
||||||
{ localContext: 'connections', target: connectedId }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(connectionDelta);
|
node.lossless.ingestDelta(connectionDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Delta } from '../src/core';
|
|
||||||
import { DefaultSchemaRegistry } from '../src/schema';
|
import { DefaultSchemaRegistry } from '../src/schema';
|
||||||
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema';
|
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema';
|
||||||
import { CommonSchemas } from '../util/schemas';
|
import { CommonSchemas } from '../util/schemas';
|
||||||
import { TypedCollectionImpl } from '../src/collections';
|
import { TypedCollectionImpl } from '../src/collections';
|
||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
|
|
||||||
describe('Nested Object Resolution', () => {
|
describe('Nested Object Resolution', () => {
|
||||||
let node: RhizomeNode;
|
let node: RhizomeNode;
|
||||||
@ -55,14 +55,10 @@ describe('Nested Object Resolution', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create friendship relationship
|
// Create friendship relationship
|
||||||
const friendshipDelta = new Delta({
|
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendshipDelta);
|
node.lossless.ingestDelta(friendshipDelta);
|
||||||
|
|
||||||
// Get Alice's lossless view
|
// Get Alice's lossless view
|
||||||
@ -107,14 +103,10 @@ describe('Nested Object Resolution', () => {
|
|||||||
// Create user with reference to non-existent friend
|
// Create user with reference to non-existent friend
|
||||||
await userCollection.put('alice', { name: 'Alice' });
|
await userCollection.put('alice', { name: 'Alice' });
|
||||||
|
|
||||||
const friendshipDelta = new Delta({
|
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'nonexistent')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'nonexistent' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendshipDelta);
|
node.lossless.ingestDelta(friendshipDelta);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
@ -162,25 +154,17 @@ describe('Nested Object Resolution', () => {
|
|||||||
await userCollection.put('charlie', { name: 'Charlie' });
|
await userCollection.put('charlie', { name: 'Charlie' });
|
||||||
|
|
||||||
// Alice's mentor is Bob
|
// Alice's mentor is Bob
|
||||||
const mentorshipDelta1 = new Delta({
|
const mentorshipDelta1 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('deep-users', 'alice', 'mentor')
|
||||||
host: node.config.peerId,
|
.addPointer('mentor', 'bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'deep-users', target: 'alice', targetContext: 'mentor' },
|
|
||||||
{ localContext: 'mentor', target: 'bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(mentorshipDelta1);
|
node.lossless.ingestDelta(mentorshipDelta1);
|
||||||
|
|
||||||
// Bob's mentor is Charlie
|
// Bob's mentor is Charlie
|
||||||
const mentorshipDelta2 = new Delta({
|
const mentorshipDelta2 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('deep-users', 'bob', 'mentor')
|
||||||
host: node.config.peerId,
|
.addPointer('mentor', 'charlie')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'deep-users', target: 'bob', targetContext: 'mentor' },
|
|
||||||
{ localContext: 'mentor', target: 'charlie' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(mentorshipDelta2);
|
node.lossless.ingestDelta(mentorshipDelta2);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
@ -246,24 +230,16 @@ describe('Nested Object Resolution', () => {
|
|||||||
await userCollection.put('bob', { name: 'Bob' });
|
await userCollection.put('bob', { name: 'Bob' });
|
||||||
|
|
||||||
// Create circular friendship: Alice -> Bob -> Alice
|
// Create circular friendship: Alice -> Bob -> Alice
|
||||||
const friendship1 = new Delta({
|
const friendship1 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendship1);
|
node.lossless.ingestDelta(friendship1);
|
||||||
|
|
||||||
const friendship2 = new Delta({
|
const friendship2 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'bob', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'alice')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'bob', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendship2);
|
node.lossless.ingestDelta(friendship2);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
@ -295,14 +271,10 @@ describe('Nested Object Resolution', () => {
|
|||||||
await userCollection.put('alice', { name: 'Alice' });
|
await userCollection.put('alice', { name: 'Alice' });
|
||||||
|
|
||||||
// Alice is friends with herself
|
// Alice is friends with herself
|
||||||
const selfFriendship = new Delta({
|
const selfFriendship = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'alice')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(selfFriendship);
|
node.lossless.ingestDelta(selfFriendship);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
@ -335,24 +307,16 @@ describe('Nested Object Resolution', () => {
|
|||||||
await userCollection.put('charlie', { name: 'Charlie' });
|
await userCollection.put('charlie', { name: 'Charlie' });
|
||||||
|
|
||||||
// Alice has multiple friends
|
// Alice has multiple friends
|
||||||
const friendship1 = new Delta({
|
const friendship1 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendship1);
|
node.lossless.ingestDelta(friendship1);
|
||||||
|
|
||||||
const friendship2 = new Delta({
|
const friendship2 = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'charlie')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'charlie' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendship2);
|
node.lossless.ingestDelta(friendship2);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
@ -405,14 +369,10 @@ describe('Nested Object Resolution', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create friendship
|
// Create friendship
|
||||||
const friendship = new Delta({
|
const friendship = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'alice', 'friends')
|
||||||
host: node.config.peerId,
|
.addPointer('friends', 'bob')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
|
|
||||||
{ localContext: 'friends', target: 'bob' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(friendship);
|
node.lossless.ingestDelta(friendship);
|
||||||
|
|
||||||
const aliceViews = node.lossless.view(['alice']);
|
const aliceViews = node.lossless.view(['alice']);
|
||||||
|
@ -3,7 +3,7 @@ import { Lossless } from '../src/views';
|
|||||||
import { DefaultSchemaRegistry } from '../src/schema';
|
import { DefaultSchemaRegistry } from '../src/schema';
|
||||||
import { SchemaBuilder, PrimitiveSchemas } from '../src/schema';
|
import { SchemaBuilder, PrimitiveSchemas } from '../src/schema';
|
||||||
import { CommonSchemas } from '../util/schemas';
|
import { CommonSchemas } from '../util/schemas';
|
||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
|
|
||||||
describe('Query Engine', () => {
|
describe('Query Engine', () => {
|
||||||
@ -48,100 +48,65 @@ describe('Query Engine', () => {
|
|||||||
|
|
||||||
async function createUser(id: string, name: string, age?: number, email?: string) {
|
async function createUser(id: string, name: string, age?: number, email?: string) {
|
||||||
// Create user entity with name
|
// Create user entity with name
|
||||||
const nameDelta = new Delta({
|
const nameDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-name-${Date.now()}`,
|
.withId(`delta-${id}-name-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'name', name, 'user')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'user', target: id, targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: name }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(nameDelta);
|
lossless.ingestDelta(nameDelta);
|
||||||
|
|
||||||
// Add age if provided
|
// Add age if provided
|
||||||
if (age !== undefined) {
|
if (age !== undefined) {
|
||||||
const ageDelta = new Delta({
|
const ageDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-age-${Date.now()}`,
|
.withId(`delta-${id}-age-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'age', age, 'user')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'user', target: id, targetContext: 'age' },
|
|
||||||
{ localContext: 'value', target: age }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(ageDelta);
|
lossless.ingestDelta(ageDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add email if provided
|
// Add email if provided
|
||||||
if (email) {
|
if (email) {
|
||||||
const emailDelta = new Delta({
|
const emailDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-email-${Date.now()}`,
|
.withId(`delta-${id}-email-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'email', email, 'user')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'user', target: id, targetContext: 'email' },
|
|
||||||
{ localContext: 'value', target: email }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(emailDelta);
|
lossless.ingestDelta(emailDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) {
|
async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) {
|
||||||
// Title delta
|
// Title delta
|
||||||
const titleDelta = new Delta({
|
const titleDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-title-${Date.now()}`,
|
.withId(`delta-${id}-title-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'title', title, 'post')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'post', target: id, targetContext: 'title' },
|
|
||||||
{ localContext: 'value', target: title }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(titleDelta);
|
lossless.ingestDelta(titleDelta);
|
||||||
|
|
||||||
// Author delta
|
// Author delta
|
||||||
const authorDelta = new Delta({
|
const authorDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-author-${Date.now()}`,
|
.withId(`delta-${id}-author-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'author', author, 'post')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'post', target: id, targetContext: 'author' },
|
|
||||||
{ localContext: 'value', target: author }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(authorDelta);
|
lossless.ingestDelta(authorDelta);
|
||||||
|
|
||||||
// Published delta
|
// Published delta
|
||||||
const publishedDelta = new Delta({
|
const publishedDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-published-${Date.now()}`,
|
.withId(`delta-${id}-published-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'published', published, 'post')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'post', target: id, targetContext: 'published' },
|
|
||||||
{ localContext: 'value', target: published }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(publishedDelta);
|
lossless.ingestDelta(publishedDelta);
|
||||||
|
|
||||||
// Views delta
|
// Views delta
|
||||||
const viewsDelta = new Delta({
|
const viewsDelta = createDelta('test', 'test-host')
|
||||||
id: `delta-${id}-views-${Date.now()}`,
|
.withId(`delta-${id}-views-${Date.now()}`)
|
||||||
creator: 'test',
|
.withTimestamp(Date.now())
|
||||||
host: 'test-host',
|
.setProperty(id, 'views', views, 'post')
|
||||||
timeCreated: Date.now(),
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: 'post', target: id, targetContext: 'views' },
|
|
||||||
{ localContext: 'value', target: views }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(viewsDelta);
|
lossless.ingestDelta(viewsDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,22 +9,12 @@ describe('Run (Orchestrated)', () => {
|
|||||||
let apiUrl: string;
|
let apiUrl: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
console.time('Test setup');
|
|
||||||
console.time('Create config');
|
|
||||||
// Configure and start the node
|
// Configure and start the node
|
||||||
const config: NodeConfig = {
|
const config: NodeConfig = {
|
||||||
id: 'app-001',
|
id: 'app-001',
|
||||||
};
|
};
|
||||||
console.timeEnd('Create config');
|
|
||||||
|
|
||||||
console.time('Start node');
|
|
||||||
nodeHandle = await orchestrator.startNode(config);
|
nodeHandle = await orchestrator.startNode(config);
|
||||||
console.timeEnd('Start node');
|
|
||||||
|
|
||||||
console.time('Get API URL');
|
|
||||||
apiUrl = nodeHandle.getApiUrl();
|
apiUrl = nodeHandle.getApiUrl();
|
||||||
console.timeEnd('Get API URL');
|
|
||||||
console.timeEnd('Test setup');
|
|
||||||
}, 60000); // Increase timeout to 60s for this hook
|
}, 60000); // Increase timeout to 60s for this hook
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -19,41 +19,26 @@ describe('Run (Two Nodes Orchestrated)', () => {
|
|||||||
const nodeIds = ['app-002-A', 'app-002-B'];
|
const nodeIds = ['app-002-A', 'app-002-B'];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
console.time('Test setup');
|
|
||||||
|
|
||||||
// Start first node
|
// Start first node
|
||||||
console.time('Create node1 config');
|
|
||||||
const node1Config: NodeConfig = {
|
const node1Config: NodeConfig = {
|
||||||
id: nodeIds[0],
|
id: nodeIds[0],
|
||||||
};
|
};
|
||||||
console.timeEnd('Create node1 config');
|
|
||||||
|
|
||||||
console.time('Start node1');
|
|
||||||
const node1 = (await orchestrator.startNode(node1Config)) as FullNodeHandle;
|
const node1 = (await orchestrator.startNode(node1Config)) as FullNodeHandle;
|
||||||
console.timeEnd('Start node1');
|
|
||||||
|
|
||||||
// Start second node with first node as bootstrap peer
|
// Start second node with first node as bootstrap peer
|
||||||
console.time('Create node2 config');
|
|
||||||
const node2Config: NodeConfig = {
|
const node2Config: NodeConfig = {
|
||||||
id: nodeIds[1],
|
id: nodeIds[1],
|
||||||
network: {
|
network: {
|
||||||
bootstrapPeers: [`localhost:${node1.getRequestPort()}`],
|
bootstrapPeers: [`localhost:${node1.getRequestPort()}`],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
console.timeEnd('Create node2 config');
|
|
||||||
|
|
||||||
console.time('Start node2');
|
|
||||||
const node2 = (await orchestrator.startNode(node2Config)) as FullNodeHandle;
|
const node2 = (await orchestrator.startNode(node2Config)) as FullNodeHandle;
|
||||||
console.timeEnd('Start node2');
|
|
||||||
|
|
||||||
nodes.push(node1, node2);
|
nodes.push(node1, node2);
|
||||||
|
|
||||||
// Connect the nodes
|
// Connect the nodes
|
||||||
console.time('Connect nodes');
|
|
||||||
await orchestrator.connectNodes(node1, node2);
|
await orchestrator.connectNodes(node1, node2);
|
||||||
console.timeEnd('Connect nodes');
|
|
||||||
|
|
||||||
console.timeEnd('Test setup');
|
|
||||||
}, 120000); // Increase timeout to 120s for this hook
|
}, 120000); // Increase timeout to 120s for this hook
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -30,7 +30,7 @@ interface ExtendedNodeStatus extends Omit<NodeStatus, 'network'> {
|
|||||||
// Set default timeout for all tests to 5 minutes
|
// Set default timeout for all tests to 5 minutes
|
||||||
jest.setTimeout(300000);
|
jest.setTimeout(300000);
|
||||||
|
|
||||||
describe('Docker Orchestrator V2', () => {
|
describe('Docker Orchestrator', () => {
|
||||||
let docker: Docker;
|
let docker: Docker;
|
||||||
let orchestrator: DockerOrchestrator;
|
let orchestrator: DockerOrchestrator;
|
||||||
let node: NodeHandle | null = null;
|
let node: NodeHandle | null = null;
|
||||||
@ -43,27 +43,31 @@ describe('Docker Orchestrator V2', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
debug('Setting up Docker client and orchestrator...');
|
debug('Setting up Docker client and orchestrator...');
|
||||||
|
|
||||||
// Initialize Docker client
|
// Initialize Docker client with increased timeout
|
||||||
docker = new Docker();
|
docker = new Docker({
|
||||||
|
timeout: 60000, // 60 second timeout for Docker operations
|
||||||
|
});
|
||||||
|
|
||||||
// Verify Docker is running
|
// Verify Docker is running
|
||||||
try {
|
try {
|
||||||
await docker.ping();
|
await docker.ping();
|
||||||
debug('Docker daemon is responding');
|
debug('Docker daemon is responding');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
debug('Docker daemon is not responding: %o', error);
|
debug('Docker daemon is not responding: %o', error);
|
||||||
throw error;
|
throw new Error(`Docker daemon is not running or not accessible: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the orchestrator with the Docker client and test image
|
// Initialize the orchestrator with the Docker client and test image
|
||||||
orchestrator = createOrchestrator('docker') as DockerOrchestrator;
|
orchestrator = createOrchestrator('docker') as DockerOrchestrator;
|
||||||
debug('Docker orchestrator initialized');
|
debug('Docker orchestrator initialized');
|
||||||
|
|
||||||
// Create a basic node config for testing
|
// Create a basic node config for testing with unique network ID
|
||||||
|
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||||
nodePort = 3000 + Math.floor(Math.random() * 1000);
|
nodePort = 3000 + Math.floor(Math.random() * 1000);
|
||||||
nodeConfig = {
|
nodeConfig = {
|
||||||
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
id: `node-${testRunId}`,
|
||||||
networkId: 'test-network',
|
networkId: `test-network-${testRunId}`,
|
||||||
port: nodePort,
|
port: nodePort,
|
||||||
resources: {
|
resources: {
|
||||||
memory: 256, // 256MB
|
memory: 256, // 256MB
|
||||||
@ -79,85 +83,8 @@ describe('Docker Orchestrator V2', () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
debug('Starting test cleanup...');
|
debug('Starting test cleanup...');
|
||||||
const cleanupPromises: Promise<unknown>[] = [];
|
|
||||||
|
|
||||||
// Helper function to clean up a node with retries
|
await orchestrator.cleanup();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('All test cleanups completed');
|
debug('All test cleanups completed');
|
||||||
}, 120000); // 2 minute timeout for afterAll
|
}, 120000); // 2 minute timeout for afterAll
|
||||||
@ -166,12 +93,19 @@ describe('Docker Orchestrator V2', () => {
|
|||||||
debug('Starting test: should start and stop a node');
|
debug('Starting test: should start and stop a node');
|
||||||
|
|
||||||
// Create a new config with a unique ID for this test
|
// Create a new config with a unique ID for this test
|
||||||
|
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||||
const testNodeConfig = {
|
const testNodeConfig = {
|
||||||
...nodeConfig,
|
...nodeConfig,
|
||||||
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
id: `node-${testRunId}`,
|
||||||
|
networkId: `test-network-${testRunId}`,
|
||||||
network: {
|
network: {
|
||||||
...nodeConfig.network,
|
...nodeConfig.network,
|
||||||
enableHttpApi: true
|
enableHttpApi: true
|
||||||
|
},
|
||||||
|
// Add retry logic for Docker operations
|
||||||
|
docker: {
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 1000
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -209,7 +143,7 @@ describe('Docker Orchestrator V2', () => {
|
|||||||
debug('Error during node cleanup: %o', e);
|
debug('Error during node cleanup: %o', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 30000); // 30 second timeout for this test
|
});
|
||||||
|
|
||||||
it('should enforce resource limits', async () => {
|
it('should enforce resource limits', async () => {
|
||||||
debug('Starting test: should enforce resource limits');
|
debug('Starting test: should enforce resource limits');
|
||||||
|
@ -10,7 +10,7 @@ import { DefaultSchemaRegistry } from '../src/schema';
|
|||||||
import { CommonSchemas } from '../util/schemas';
|
import { CommonSchemas } from '../util/schemas';
|
||||||
import { TypedCollectionImpl, SchemaValidationError } from '../src/collections';
|
import { TypedCollectionImpl, SchemaValidationError } from '../src/collections';
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
|
|
||||||
describe('Schema System', () => {
|
describe('Schema System', () => {
|
||||||
let schemaRegistry: DefaultSchemaRegistry;
|
let schemaRegistry: DefaultSchemaRegistry;
|
||||||
@ -21,7 +21,9 @@ describe('Schema System', () => {
|
|||||||
node = new RhizomeNode();
|
node = new RhizomeNode();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('Schema Builder', () => {
|
describe('Schema Builder', () => {
|
||||||
|
|
||||||
it('should create a basic schema', () => {
|
it('should create a basic schema', () => {
|
||||||
const schema = SchemaBuilder
|
const schema = SchemaBuilder
|
||||||
.create('user')
|
.create('user')
|
||||||
@ -308,14 +310,10 @@ describe('Schema System', () => {
|
|||||||
await collection.put('user2', { name: 'Bob' });
|
await collection.put('user2', { name: 'Bob' });
|
||||||
|
|
||||||
// Manually create an invalid entity by bypassing validation
|
// Manually create an invalid entity by bypassing validation
|
||||||
const invalidDelta = new Delta({
|
const invalidDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'user3', 'email')
|
||||||
host: node.config.peerId,
|
.addPointer('email', 'invalid@test.com')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'user3', targetContext: 'email' },
|
|
||||||
{ localContext: 'email', target: 'invalid@test.com' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(invalidDelta);
|
node.lossless.ingestDelta(invalidDelta);
|
||||||
|
|
||||||
const stats = collection.getValidationStats();
|
const stats = collection.getValidationStats();
|
||||||
@ -337,14 +335,10 @@ describe('Schema System', () => {
|
|||||||
await collection.put('user2', { name: 'Bob' });
|
await collection.put('user2', { name: 'Bob' });
|
||||||
|
|
||||||
// Create invalid entity manually
|
// Create invalid entity manually
|
||||||
const invalidDelta = new Delta({
|
const invalidDelta = createDelta(node.config.creator, node.config.peerId)
|
||||||
creator: node.config.creator,
|
.addPointer('users', 'user3', 'age')
|
||||||
host: node.config.peerId,
|
.addPointer('age', 'not-a-number')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'users', target: 'user3', targetContext: 'age' },
|
|
||||||
{ localContext: 'age', target: 'not-a-number' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
node.lossless.ingestDelta(invalidDelta);
|
node.lossless.ingestDelta(invalidDelta);
|
||||||
|
|
||||||
const validIds = collection.getValidEntities();
|
const validIds = collection.getValidEntities();
|
||||||
|
@ -1,39 +1,25 @@
|
|||||||
import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage';
|
import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage';
|
||||||
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import { Delta } from '../src/core';
|
import { Delta } from '../src/core';
|
||||||
import { DeltaQueryStorage } from '../src/storage/interface';
|
import { DeltaQueryStorage } from '../src/storage/interface';
|
||||||
|
|
||||||
describe('Delta Storage', () => {
|
describe('Delta Storage', () => {
|
||||||
const testDeltas = [
|
const testDeltas = [
|
||||||
new Delta({
|
createDelta('alice', 'host1')
|
||||||
id: 'delta1',
|
.withId('delta1')
|
||||||
creator: 'alice',
|
.withTimestamp(Date.now() - 1000)
|
||||||
host: 'host1',
|
.setProperty('user1', 'name', 'Alice', 'user')
|
||||||
timeCreated: Date.now() - 1000,
|
.buildV1(),
|
||||||
pointers: [
|
createDelta('bob', 'host1')
|
||||||
{ localContext: 'user', target: 'user1', targetContext: 'name' },
|
.withId('delta2')
|
||||||
{ localContext: 'value', target: 'Alice' }
|
.withTimestamp(Date.now() - 500)
|
||||||
]
|
.setProperty('user1', 'age', 25, 'user')
|
||||||
}),
|
.buildV1(),
|
||||||
new Delta({
|
createDelta('alice', 'host2')
|
||||||
id: 'delta2',
|
.withId('delta3')
|
||||||
creator: 'bob',
|
.withTimestamp(Date.now())
|
||||||
host: 'host1',
|
.setProperty('user2', 'name', 'Bob', 'user')
|
||||||
timeCreated: Date.now() - 500,
|
.buildV1()
|
||||||
pointers: [
|
|
||||||
{ localContext: 'user', target: 'user1', targetContext: 'age' },
|
|
||||||
{ localContext: 'value', target: 25 }
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new Delta({
|
|
||||||
id: 'delta3',
|
|
||||||
creator: 'alice',
|
|
||||||
host: 'host2',
|
|
||||||
timeCreated: Date.now(),
|
|
||||||
pointers: [
|
|
||||||
{ localContext: 'user', target: 'user2', targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: 'Bob' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Memory Storage', () => {
|
describe('Memory Storage', () => {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
RhizomeNode,
|
RhizomeNode,
|
||||||
Lossless,
|
Lossless,
|
||||||
Delta,
|
|
||||||
TimestampResolver,
|
TimestampResolver,
|
||||||
CreatorIdTimestampResolver,
|
CreatorIdTimestampResolver,
|
||||||
DeltaIdTimestampResolver,
|
DeltaIdTimestampResolver,
|
||||||
HostIdTimestampResolver,
|
HostIdTimestampResolver,
|
||||||
LexicographicTimestampResolver
|
LexicographicTimestampResolver
|
||||||
} from "../src";
|
} from "../src";
|
||||||
|
import { createDelta } from "../src/core/delta-builder";
|
||||||
|
|
||||||
describe('Timestamp Resolvers', () => {
|
describe('Timestamp Resolvers', () => {
|
||||||
let node: RhizomeNode;
|
let node: RhizomeNode;
|
||||||
@ -21,36 +21,22 @@ describe('Timestamp Resolvers', () => {
|
|||||||
describe('Basic Timestamp Resolution', () => {
|
describe('Basic Timestamp Resolution', () => {
|
||||||
test('should resolve by most recent timestamp', () => {
|
test('should resolve by most recent timestamp', () => {
|
||||||
// Add older delta
|
// Add older delta
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add newer delta
|
// Add newer delta
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
||||||
creator: 'user2',
|
.withId('delta2')
|
||||||
host: 'host2',
|
.withTimestamp(2000)
|
||||||
id: 'delta2',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 2000,
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new TimestampResolver(lossless);
|
const resolver = new TimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -61,34 +47,20 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should handle multiple entities with different timestamps', () => {
|
test('should handle multiple entities with different timestamps', () => {
|
||||||
// Entity1 - older value
|
// Entity1 - older value
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(1000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity1', 'value')
|
||||||
timeCreated: 1000,
|
.addPointer('value', 100)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Entity2 - newer value
|
// Entity2 - newer value
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withTimestamp(2000)
|
||||||
host: 'host1',
|
.addPointer('collection', 'entity2', 'value')
|
||||||
timeCreated: 2000,
|
.addPointer('value', 200)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity2",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 200
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new TimestampResolver(lossless);
|
const resolver = new TimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -102,35 +74,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
describe('Tie-Breaking Strategies', () => {
|
describe('Tie-Breaking Strategies', () => {
|
||||||
test('should break ties using creator-id strategy', () => {
|
test('should break ties using creator-id strategy', () => {
|
||||||
// Two deltas with same timestamp, different creators
|
// Two deltas with same timestamp, different creators
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
||||||
creator: 'user_z', // Lexicographically later
|
.withId('delta1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
||||||
creator: 'user_a', // Lexicographically earlier
|
.withId('delta2')
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta2',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CreatorIdTimestampResolver(lossless);
|
const resolver = new CreatorIdTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -142,35 +100,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should break ties using delta-id strategy', () => {
|
test('should break ties using delta-id strategy', () => {
|
||||||
// Two deltas with same timestamp, different delta IDs
|
// Two deltas with same timestamp, different delta IDs
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta_a') // Lexicographically earlier
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta_a', // Lexicographically earlier
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta_z') // Lexicographically later
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta_z', // Lexicographically later
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new DeltaIdTimestampResolver(lossless);
|
const resolver = new DeltaIdTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -182,35 +126,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should break ties using host-id strategy', () => {
|
test('should break ties using host-id strategy', () => {
|
||||||
// Two deltas with same timestamp, different hosts
|
// Two deltas with same timestamp, different hosts
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host_z') // Lexicographically later
|
||||||
creator: 'user1',
|
.withId('delta1')
|
||||||
host: 'host_z', // Lexicographically later
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host_a') // Lexicographically earlier
|
||||||
creator: 'user1',
|
.withId('delta2')
|
||||||
host: 'host_a', // Lexicographically earlier
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta2',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new HostIdTimestampResolver(lossless);
|
const resolver = new HostIdTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -222,35 +152,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should break ties using lexicographic strategy with string values', () => {
|
test('should break ties using lexicographic strategy with string values', () => {
|
||||||
// Two deltas with same timestamp, different string values
|
// Two deltas with same timestamp, different string values
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: 1000,
|
.addPointer('name', 'alice')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'alice'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta2')
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta2',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('name', 'bob')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'bob'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new LexicographicTimestampResolver(lossless);
|
const resolver = new LexicographicTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -262,35 +178,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should break ties using lexicographic strategy with numeric values (falls back to delta ID)', () => {
|
test('should break ties using lexicographic strategy with numeric values (falls back to delta ID)', () => {
|
||||||
// Two deltas with same timestamp, numeric values (should fall back to delta ID comparison)
|
// Two deltas with same timestamp, numeric values (should fall back to delta ID comparison)
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta_a') // Lexicographically earlier
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta_a', // Lexicographically earlier
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000,
|
.addPointer('score', 100)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta_z') // Lexicographically later
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta_z', // Lexicographically later
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('score', 200)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 200
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new LexicographicTimestampResolver(lossless);
|
const resolver = new LexicographicTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -304,35 +206,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
describe('Complex Tie-Breaking Scenarios', () => {
|
describe('Complex Tie-Breaking Scenarios', () => {
|
||||||
test('should handle multiple properties with different tie-breaking outcomes', () => {
|
test('should handle multiple properties with different tie-breaking outcomes', () => {
|
||||||
// Add deltas for multiple properties with same timestamp
|
// Add deltas for multiple properties with same timestamp
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
||||||
creator: 'user_a',
|
.withId('delta_z')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta_z',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: 1000,
|
.addPointer('name', 'alice')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'alice'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
||||||
creator: 'user_z',
|
.withId('delta_a')
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Same timestamp
|
||||||
id: 'delta_a',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: 1000, // Same timestamp
|
.addPointer('name', 'bob')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'bob'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const creatorResolver = new CreatorIdTimestampResolver(lossless);
|
const creatorResolver = new CreatorIdTimestampResolver(lossless);
|
||||||
const deltaResolver = new DeltaIdTimestampResolver(lossless);
|
const deltaResolver = new DeltaIdTimestampResolver(lossless);
|
||||||
@ -352,36 +240,22 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
test('should work consistently with timestamp priority over tie-breaking', () => {
|
test('should work consistently with timestamp priority over tie-breaking', () => {
|
||||||
// Add older delta with "better" tie-breaking attributes
|
// Add older delta with "better" tie-breaking attributes
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
||||||
creator: 'user_z', // Would win in creator tie-breaking
|
.withId('delta_z') // Would win in delta ID tie-breaking
|
||||||
host: 'host1',
|
.withTimestamp(1000) // Older timestamp
|
||||||
id: 'delta_z', // Would win in delta ID tie-breaking
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1000, // Older timestamp
|
.addPointer('score', 10)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 10
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add newer delta with "worse" tie-breaking attributes
|
// Add newer delta with "worse" tie-breaking attributes
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
||||||
creator: 'user_a', // Would lose in creator tie-breaking
|
.withId('delta_a') // Would lose in delta ID tie-breaking
|
||||||
host: 'host1',
|
.withTimestamp(2000) // Newer timestamp
|
||||||
id: 'delta_a', // Would lose in delta ID tie-breaking
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 2000, // Newer timestamp
|
.addPointer('score', 20)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 20
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new CreatorIdTimestampResolver(lossless);
|
const resolver = new CreatorIdTimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -394,20 +268,13 @@ describe('Timestamp Resolvers', () => {
|
|||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
test('should handle single delta correctly', () => {
|
test('should handle single delta correctly', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'value')
|
||||||
timeCreated: 1000,
|
.addPointer('value', 42)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "value"
|
|
||||||
}, {
|
|
||||||
localContext: "value",
|
|
||||||
target: 42
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new TimestampResolver(lossless, 'creator-id');
|
const resolver = new TimestampResolver(lossless, 'creator-id');
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
@ -417,35 +284,21 @@ describe('Timestamp Resolvers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle mixed value types correctly', () => {
|
test('should handle mixed value types correctly', () => {
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta1')
|
||||||
host: 'host1',
|
.withTimestamp(1000)
|
||||||
id: 'delta1',
|
.addPointer('collection', 'entity1', 'name')
|
||||||
timeCreated: 1000,
|
.addPointer('name', 'test')
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "name"
|
|
||||||
}, {
|
|
||||||
localContext: "name",
|
|
||||||
target: 'test'
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.withId('delta2')
|
||||||
host: 'host1',
|
.withTimestamp(1001)
|
||||||
id: 'delta2',
|
.addPointer('collection', 'entity1', 'score')
|
||||||
timeCreated: 1001,
|
.addPointer('score', 100)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: "collection",
|
);
|
||||||
target: "entity1",
|
|
||||||
targetContext: "score"
|
|
||||||
}, {
|
|
||||||
localContext: "score",
|
|
||||||
target: 100
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
|
|
||||||
const resolver = new TimestampResolver(lossless);
|
const resolver = new TimestampResolver(lossless);
|
||||||
const result = resolver.resolve();
|
const result = resolver.resolve();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Delta } from '../src/core';
|
import { createDelta } from '../src/core/delta-builder';
|
||||||
import { Lossless } from '../src/views';
|
import { Lossless } from '../src/views';
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { DeltaFilter } from '../src/core';
|
import { DeltaFilter } from '../src/core';
|
||||||
@ -17,36 +17,21 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-123';
|
const transactionId = 'tx-123';
|
||||||
|
|
||||||
// Create a delta that declares a transaction with size 3
|
// Create a delta that declares a transaction with size 3
|
||||||
const txDeclaration = new Delta({
|
const txDeclaration = createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 3)
|
||||||
host: 'host1',
|
.buildV1();
|
||||||
pointers: [
|
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 3 }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create first delta in transaction
|
// Create first delta in transaction
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('user123', 'name', 'Alice')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'name', target: 'user123', targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: 'Alice' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create second delta in transaction
|
// Create second delta in transaction
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('user123', 'age', 25)
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'age', target: 'user123', targetContext: 'age' },
|
|
||||||
{ localContext: 'value', target: 25 }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ingest transaction declaration and first two deltas
|
// Ingest transaction declaration and first two deltas
|
||||||
lossless.ingestDelta(txDeclaration);
|
lossless.ingestDelta(txDeclaration);
|
||||||
@ -58,15 +43,10 @@ describe('Transactions', () => {
|
|||||||
expect(view.user123).toBeUndefined();
|
expect(view.user123).toBeUndefined();
|
||||||
|
|
||||||
// Add the third delta to complete the transaction
|
// Add the third delta to complete the transaction
|
||||||
const delta3 = new Delta({
|
const delta3 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('user123', 'email', 'alice@example.com')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'email', target: 'user123', targetContext: 'email' },
|
|
||||||
{ localContext: 'value', target: 'alice@example.com' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
lossless.ingestDelta(delta3);
|
lossless.ingestDelta(delta3);
|
||||||
|
|
||||||
@ -83,44 +63,28 @@ describe('Transactions', () => {
|
|||||||
const tx2 = 'tx-002';
|
const tx2 = 'tx-002';
|
||||||
|
|
||||||
// Declare two transactions
|
// Declare two transactions
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(tx1, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: tx1, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(tx2, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: tx2, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add deltas for both transactions
|
// Add deltas for both transactions
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(tx1)
|
||||||
host: 'host1',
|
.setProperty('order1', 'status', 'pending')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'status', target: 'order1', targetContext: 'status' },
|
|
||||||
{ localContext: 'value', target: 'pending' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
||||||
creator: 'user2',
|
.inTransaction(tx2)
|
||||||
host: 'host2',
|
.setProperty('order2', 'status', 'shipped')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'status', target: 'order2', targetContext: 'status' },
|
|
||||||
{ localContext: 'value', target: 'shipped' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Neither transaction is complete
|
// Neither transaction is complete
|
||||||
let view = lossless.view(['order1', 'order2']);
|
let view = lossless.view(['order1', 'order2']);
|
||||||
@ -128,15 +92,11 @@ describe('Transactions', () => {
|
|||||||
expect(view.order2).toBeUndefined();
|
expect(view.order2).toBeUndefined();
|
||||||
|
|
||||||
// Complete tx1
|
// Complete tx1
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(tx1)
|
||||||
host: 'host1',
|
.setProperty('order1', 'total', 100)
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'total', target: 'order1', targetContext: 'total' },
|
|
||||||
{ localContext: 'value', target: 100 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// tx1 is complete, tx2 is not
|
// tx1 is complete, tx2 is not
|
||||||
view = lossless.view(['order1', 'order2']);
|
view = lossless.view(['order1', 'order2']);
|
||||||
@ -146,15 +106,11 @@ describe('Transactions', () => {
|
|||||||
expect(view.order2).toBeUndefined();
|
expect(view.order2).toBeUndefined();
|
||||||
|
|
||||||
// Complete tx2
|
// Complete tx2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
||||||
creator: 'user2',
|
.inTransaction(tx2)
|
||||||
host: 'host2',
|
.setProperty('order2', 'tracking', 'TRACK123')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'tracking', target: 'order2', targetContext: 'tracking' },
|
|
||||||
{ localContext: 'value', target: 'TRACK123' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Both transactions complete
|
// Both transactions complete
|
||||||
view = lossless.view(['order1', 'order2']);
|
view = lossless.view(['order1', 'order2']);
|
||||||
@ -168,35 +124,23 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-filter-test';
|
const transactionId = 'tx-filter-test';
|
||||||
|
|
||||||
// Create transaction with 2 deltas
|
// Create transaction with 2 deltas
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add both deltas
|
// Add both deltas
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('doc1', 'type', 'report')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'type', target: 'doc1', targetContext: 'type' },
|
|
||||||
{ localContext: 'value', target: 'report' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
||||||
creator: 'user2',
|
.inTransaction(transactionId)
|
||||||
host: 'host2',
|
.setProperty('doc1', 'author', 'Bob')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'author', target: 'doc1', targetContext: 'author' },
|
|
||||||
{ localContext: 'value', target: 'Bob' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Create a filter that only accepts deltas from user1
|
// Create a filter that only accepts deltas from user1
|
||||||
const userFilter: DeltaFilter = (delta) => delta.creator === 'user1';
|
const userFilter: DeltaFilter = (delta) => delta.creator === 'user1';
|
||||||
@ -215,37 +159,27 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-multi-entity';
|
const transactionId = 'tx-multi-entity';
|
||||||
|
|
||||||
// Transaction that updates multiple entities atomically
|
// Transaction that updates multiple entities atomically
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 3)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 3 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Transfer money from account1 to account2
|
// Transfer money from account1 to account2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('bank', 'host1')
|
||||||
creator: 'bank',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.addPointer('balance', 'account1', 'balance')
|
||||||
pointers: [
|
.addPointer('value', 900)
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.addPointer('operation', 'debit')
|
||||||
{ localContext: 'balance', target: 'account1', targetContext: 'balance' },
|
.buildV1()
|
||||||
{ localContext: 'value', target: 900 },
|
);
|
||||||
{ localContext: 'operation', target: 'debit' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('bank', 'host1')
|
||||||
creator: 'bank',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.addPointer('balance', 'account2', 'balance')
|
||||||
pointers: [
|
.addPointer('value', 1100)
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.addPointer('operation', 'credit')
|
||||||
{ localContext: 'balance', target: 'account2', targetContext: 'balance' },
|
.buildV1()
|
||||||
{ localContext: 'value', target: 1100 },
|
);
|
||||||
{ localContext: 'operation', target: 'credit' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Transaction incomplete - no entities should show updates
|
// Transaction incomplete - no entities should show updates
|
||||||
let view = lossless.view(['account1', 'account2']);
|
let view = lossless.view(['account1', 'account2']);
|
||||||
@ -253,17 +187,14 @@ describe('Transactions', () => {
|
|||||||
expect(view.account2).toBeUndefined();
|
expect(view.account2).toBeUndefined();
|
||||||
|
|
||||||
// Complete transaction with audit log
|
// Complete transaction with audit log
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('bank', 'host1')
|
||||||
creator: 'bank',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.addPointer('transfer', 'transfer123', 'details')
|
||||||
pointers: [
|
.addPointer('from', 'account1')
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
.addPointer('to', 'account2')
|
||||||
{ localContext: 'transfer', target: 'transfer123', targetContext: 'details' },
|
.addPointer('amount', 100)
|
||||||
{ localContext: 'from', target: 'account1' },
|
.buildV1()
|
||||||
{ localContext: 'to', target: 'account2' },
|
);
|
||||||
{ localContext: 'amount', target: 100 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// All entities should now be visible
|
// All entities should now be visible
|
||||||
view = lossless.view(['account1', 'account2', 'transfer123']);
|
view = lossless.view(['account1', 'account2', 'transfer123']);
|
||||||
@ -285,40 +216,26 @@ describe('Transactions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add first delta
|
// Add first delta
|
||||||
const delta1 = new Delta({
|
const delta1 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'field1', 'value1')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'field1', target: 'entity1', targetContext: 'field1' },
|
|
||||||
{ localContext: 'value', target: 'value1' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(delta1);
|
lossless.ingestDelta(delta1);
|
||||||
|
|
||||||
// No events should be emitted yet
|
// No events should be emitted yet
|
||||||
expect(updateEvents).toHaveLength(0);
|
expect(updateEvents).toHaveLength(0);
|
||||||
|
|
||||||
// Add second delta to complete transaction
|
// Add second delta to complete transaction
|
||||||
const delta2 = new Delta({
|
const delta2 = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'field2', 'value2')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
|
||||||
{ localContext: 'field2', target: 'entity1', targetContext: 'field2' },
|
|
||||||
{ localContext: 'value', target: 'value2' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
lossless.ingestDelta(delta2);
|
lossless.ingestDelta(delta2);
|
||||||
|
|
||||||
// Wait for async event processing
|
// Wait for async event processing
|
||||||
@ -339,25 +256,17 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-wait';
|
const transactionId = 'tx-wait';
|
||||||
|
|
||||||
// Create transaction
|
// Create transaction
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add first delta
|
// Add first delta
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('job1', 'status', 'processing')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'status', target: 'job1', targetContext: 'status' },
|
|
||||||
{ localContext: 'value', target: 'processing' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Start waiting for transaction
|
// Start waiting for transaction
|
||||||
const waitPromise = lossless.transactions.waitFor(transactionId);
|
const waitPromise = lossless.transactions.waitFor(transactionId);
|
||||||
@ -369,15 +278,11 @@ describe('Transactions', () => {
|
|||||||
expect(isResolved).toBe(false);
|
expect(isResolved).toBe(false);
|
||||||
|
|
||||||
// Complete transaction
|
// Complete transaction
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('job1', 'status', 'completed')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'status', target: 'job1', targetContext: 'status' },
|
|
||||||
{ localContext: 'value', target: 'completed' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Wait should now resolve
|
// Wait should now resolve
|
||||||
await waitPromise;
|
await waitPromise;
|
||||||
@ -391,14 +296,10 @@ describe('Transactions', () => {
|
|||||||
|
|
||||||
it('should handle non-transactional deltas normally', () => {
|
it('should handle non-transactional deltas normally', () => {
|
||||||
// Regular delta without transaction
|
// Regular delta without transaction
|
||||||
const regularDelta = new Delta({
|
const regularDelta = createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.addPointer('name', 'user456', 'name')
|
||||||
host: 'host1',
|
.addPointer('value', 'Charlie')
|
||||||
pointers: [
|
.buildV1();
|
||||||
{ localContext: 'name', target: 'user456', targetContext: 'name' },
|
|
||||||
{ localContext: 'value', target: 'Charlie' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateEvents: string[] = [];
|
const updateEvents: string[] = [];
|
||||||
lossless.eventStream.on('updated', (entityId) => {
|
lossless.eventStream.on('updated', (entityId) => {
|
||||||
@ -422,33 +323,23 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-resize';
|
const transactionId = 'tx-resize';
|
||||||
|
|
||||||
// Initially declare transaction with size 2
|
// Initially declare transaction with size 2
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 2)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 2 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add 2 deltas
|
// Add 2 deltas
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('cart1', 'items', 'item1')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'item1', target: 'cart1', targetContext: 'items' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('cart1', 'items', 'item2')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'item2', target: 'cart1', targetContext: 'items' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Transaction should be complete
|
// Transaction should be complete
|
||||||
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
||||||
@ -462,15 +353,11 @@ describe('Transactions', () => {
|
|||||||
const transactionId = 'tx-no-size';
|
const transactionId = 'tx-no-size';
|
||||||
|
|
||||||
// Add delta with transaction reference but no size declaration
|
// Add delta with transaction reference but no size declaration
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
||||||
creator: 'user1',
|
.inTransaction(transactionId)
|
||||||
host: 'host1',
|
.setProperty('entity1', 'data', 'test')
|
||||||
pointers: [
|
.buildV1()
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
|
);
|
||||||
{ localContext: 'data', target: 'entity1', targetContext: 'data' },
|
|
||||||
{ localContext: 'value', target: 'test' }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Transaction should not be complete (no size)
|
// Transaction should not be complete (no size)
|
||||||
expect(lossless.transactions.isComplete(transactionId)).toBe(false);
|
expect(lossless.transactions.isComplete(transactionId)).toBe(false);
|
||||||
@ -480,14 +367,10 @@ describe('Transactions', () => {
|
|||||||
expect(view.entity1).toBeUndefined();
|
expect(view.entity1).toBeUndefined();
|
||||||
|
|
||||||
// Declare size after the fact
|
// Declare size after the fact
|
||||||
lossless.ingestDelta(new Delta({
|
lossless.ingestDelta(createDelta('system', 'host1')
|
||||||
creator: 'system',
|
.declareTransaction(transactionId, 1)
|
||||||
host: 'host1',
|
.buildV1()
|
||||||
pointers: [
|
);
|
||||||
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
|
|
||||||
{ localContext: 'size', target: 1 }
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Now transaction should be complete
|
// Now transaction should be complete
|
||||||
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"coverage": "./scripts/coverage.sh",
|
"coverage": "./scripts/coverage.sh",
|
||||||
"coverage-report": "npm run test -- --coverage --coverageDirectory=coverage",
|
"coverage-report": "npm run test -- --coverage --coverageDirectory=coverage",
|
||||||
"example-app": "node dist/examples/app.js"
|
"example-app": "node dist/examples/app.js",
|
||||||
|
"stop-all": "docker ps -a --filter \"name=^/rhizome-node-\" --format {{.Names}} | xargs -r docker stop",
|
||||||
|
"build-test-image": "docker build -t rhizome-node-test -f Dockerfile.test ."
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
@ -35,11 +37,10 @@
|
|||||||
"forceExit": true,
|
"forceExit": true,
|
||||||
"verbose": true,
|
"verbose": true,
|
||||||
"testEnvironmentOptions": {
|
"testEnvironmentOptions": {
|
||||||
"NODE_ENV": "test",
|
"NODE_ENV": "test"
|
||||||
"DEBUG": "rz:*"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"author": "Taliesin (Ladd) <ladd@dgov.io>",
|
"author": "Lentil <lentil@laddhoffman.com>",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dockerode": "^3.3.40",
|
"@types/dockerode": "^3.3.40",
|
||||||
|
@ -2,6 +2,7 @@ import Debug from 'debug';
|
|||||||
import {randomUUID} from "node:crypto";
|
import {randomUUID} from "node:crypto";
|
||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
import {Delta} from "../core/delta";
|
import {Delta} from "../core/delta";
|
||||||
|
import {createDelta} from "../core/delta-builder";
|
||||||
import {Entity, EntityProperties} from "../core/entity";
|
import {Entity, EntityProperties} from "../core/entity";
|
||||||
import {ResolvedViewOne} from '../views/resolvers/last-write-wins';
|
import {ResolvedViewOne} from '../views/resolvers/last-write-wins';
|
||||||
import {RhizomeNode} from "../node";
|
import {RhizomeNode} from "../node";
|
||||||
@ -72,18 +73,11 @@ export abstract class Collection<View> {
|
|||||||
if (key === 'id') return;
|
if (key === 'id') return;
|
||||||
|
|
||||||
if (oldProperties[key] !== value && host && creator) {
|
if (oldProperties[key] !== value && host && creator) {
|
||||||
deltas.push(new Delta({
|
deltas.push(
|
||||||
creator,
|
createDelta(creator, host)
|
||||||
host,
|
.setProperty(entityId, key, value, this.name)
|
||||||
pointers: [{
|
.buildV1()
|
||||||
localContext: this.name,
|
);
|
||||||
target: entityId,
|
|
||||||
targetContext: key
|
|
||||||
}, {
|
|
||||||
localContext: key,
|
|
||||||
target: value
|
|
||||||
}]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,18 +85,10 @@ export abstract class Collection<View> {
|
|||||||
|
|
||||||
if (deltas.length > 1) {
|
if (deltas.length > 1) {
|
||||||
// We can generate a separate delta describing this transaction
|
// We can generate a separate delta describing this transaction
|
||||||
transactionDelta = new Delta({
|
transactionDelta = createDelta(creator, host)
|
||||||
creator,
|
.addPointer('_transaction', transactionId, 'size')
|
||||||
host,
|
.addPointer('size', deltas.length)
|
||||||
pointers: [{
|
.buildV1();
|
||||||
localContext: "_transaction",
|
|
||||||
target: transactionId,
|
|
||||||
targetContext: "size"
|
|
||||||
}, {
|
|
||||||
localContext: "size",
|
|
||||||
target: deltas.length
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also need to annotate the deltas with the transactionId
|
// Also need to annotate the deltas with the transactionId
|
||||||
for (const delta of deltas) {
|
for (const delta of deltas) {
|
||||||
|
141
src/core/delta-builder.ts
Normal file
141
src/core/delta-builder.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { DeltaV1, DeltaV2 } from './delta';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import Debug from 'debug';
|
||||||
|
const debug = Debug('rz:delta-builder');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fluent builder for creating Delta objects with proper validation and type safety.
|
||||||
|
* Supports both V1 and V2 delta formats.
|
||||||
|
*/
|
||||||
|
export class DeltaBuilder {
|
||||||
|
private id: string;
|
||||||
|
private timeCreated?: number;
|
||||||
|
private host: string;
|
||||||
|
private creator: string;
|
||||||
|
private pointers: Record<string, any> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DeltaBuilder instance
|
||||||
|
* @param creator - The ID of the entity creating this delta
|
||||||
|
* @param host - The host where this delta is being created
|
||||||
|
*/
|
||||||
|
constructor(creator: string, host: string) {
|
||||||
|
this.id = randomUUID();
|
||||||
|
this.creator = creator;
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom ID for the delta
|
||||||
|
*/
|
||||||
|
withId(id: string): this {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom creation timestamp
|
||||||
|
*/
|
||||||
|
withTimestamp(timestamp: number): this {
|
||||||
|
this.timeCreated = timestamp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the transaction ID for this delta
|
||||||
|
*/
|
||||||
|
inTransaction(transactionId: string): this {
|
||||||
|
this.addPointer('_transaction', transactionId, 'deltas');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare a transaction with a size
|
||||||
|
* @param transactionId The ID of the transaction
|
||||||
|
* @param size The size of the transaction
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
declareTransaction(transactionId: string, size: number): this {
|
||||||
|
this.setProperty(transactionId, 'size', size, '_transaction');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this delta as a negation of another delta
|
||||||
|
*/
|
||||||
|
negate(deltaId: string): this {
|
||||||
|
this.addPointer('_negates', deltaId, 'negated_by');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a pointer to the delta
|
||||||
|
* @param localContext The local context for the pointer
|
||||||
|
* @param target The target value (string, number, boolean, or null)
|
||||||
|
* @param targetContext Optional target context for the pointer
|
||||||
|
*/
|
||||||
|
addPointer(localContext: string, target: string | number | boolean | null, targetContext?: string): this {
|
||||||
|
if (targetContext && typeof target === 'string') {
|
||||||
|
this.pointers[localContext] = { [target]: targetContext };
|
||||||
|
} else {
|
||||||
|
this.pointers[localContext] = target;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a property on an entity
|
||||||
|
*/
|
||||||
|
setProperty(entityId: string, property: string, value: string | number | boolean | null, entityLabel = "entity"): this {
|
||||||
|
this.addPointer(entityLabel, entityId, property)
|
||||||
|
this.addPointer(property, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a relationship between two entities
|
||||||
|
*/
|
||||||
|
relate(sourceId: string, relationship: string, targetId: string): this {
|
||||||
|
this.pointers[relationship] = { [targetId]: relationship };
|
||||||
|
this.pointers.source = { [sourceId]: relationship };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return a DeltaV2 instance
|
||||||
|
*/
|
||||||
|
buildV2(): DeltaV2 {
|
||||||
|
// For V2, we'll store transaction and negation info in the pointers object
|
||||||
|
const pointers = { ...this.pointers };
|
||||||
|
|
||||||
|
// Create the delta with all pointers
|
||||||
|
return new DeltaV2({
|
||||||
|
id: this.id,
|
||||||
|
host: this.host,
|
||||||
|
creator: this.creator,
|
||||||
|
timeCreated: this.timeCreated,
|
||||||
|
pointers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return a DeltaV1 instance
|
||||||
|
*/
|
||||||
|
buildV1(): DeltaV1 {
|
||||||
|
return this.buildV2().toV1();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default to V1 for now
|
||||||
|
*/
|
||||||
|
build(): DeltaV1 {
|
||||||
|
return this.buildV1();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DeltaBuilder instance (convenience function)
|
||||||
|
*/
|
||||||
|
export function createDelta(creator: string, host: string): DeltaBuilder {
|
||||||
|
return new DeltaBuilder(creator, host);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
export * from './delta';
|
export * from './delta';
|
||||||
|
export * from './delta-builder';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
export { Entity } from './entity';
|
export { Entity } from './entity';
|
@ -1,21 +1,17 @@
|
|||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
import { Delta, DeltaID } from '../core/delta';
|
import { Delta, DeltaID } from '../core/delta';
|
||||||
|
import { createDelta } from '../core/delta-builder';
|
||||||
import { CreatorID, HostID } from '../core/types';
|
import { CreatorID, HostID } from '../core/types';
|
||||||
|
|
||||||
const debug = Debug('rz:negation');
|
const debug = Debug('rz:negation');
|
||||||
|
|
||||||
// Negation-specific types
|
// Negation-specific types
|
||||||
export interface NegationPointer {
|
export interface NegationPointer {
|
||||||
localContext: 'negates';
|
localContext: '_negates';
|
||||||
target: DeltaID;
|
target: DeltaID;
|
||||||
targetContext: 'negated_by';
|
targetContext: 'negated_by';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NegationDelta extends Delta {
|
|
||||||
isNegation: true;
|
|
||||||
negatedDeltaId: DeltaID;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for creating and identifying negation deltas
|
// Helper functions for creating and identifying negation deltas
|
||||||
export class NegationHelper {
|
export class NegationHelper {
|
||||||
|
|
||||||
@ -26,19 +22,10 @@ export class NegationHelper {
|
|||||||
deltaToNegate: DeltaID,
|
deltaToNegate: DeltaID,
|
||||||
creator: CreatorID,
|
creator: CreatorID,
|
||||||
host: HostID
|
host: HostID
|
||||||
): NegationDelta {
|
): Delta {
|
||||||
const negationDelta = new Delta({
|
const negationDelta = createDelta(creator, host)
|
||||||
creator,
|
.negate(deltaToNegate)
|
||||||
host,
|
.buildV1();
|
||||||
pointers: [{
|
|
||||||
localContext: 'negates',
|
|
||||||
target: deltaToNegate,
|
|
||||||
targetContext: 'negated_by'
|
|
||||||
}]
|
|
||||||
}) as NegationDelta;
|
|
||||||
|
|
||||||
negationDelta.isNegation = true;
|
|
||||||
negationDelta.negatedDeltaId = deltaToNegate;
|
|
||||||
|
|
||||||
debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`);
|
debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`);
|
||||||
return negationDelta;
|
return negationDelta;
|
||||||
@ -47,9 +34,9 @@ export class NegationHelper {
|
|||||||
/**
|
/**
|
||||||
* Check if a delta is a negation delta
|
* Check if a delta is a negation delta
|
||||||
*/
|
*/
|
||||||
static isNegationDelta(delta: Delta): delta is NegationDelta {
|
static isNegationDelta(delta: Delta): boolean {
|
||||||
return delta.pointers.some(pointer =>
|
return delta.pointers.some(pointer =>
|
||||||
pointer.localContext === 'negates' &&
|
pointer.localContext === '_negates' &&
|
||||||
pointer.targetContext === 'negated_by'
|
pointer.targetContext === 'negated_by'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -59,7 +46,7 @@ export class NegationHelper {
|
|||||||
*/
|
*/
|
||||||
static getNegatedDeltaId(negationDelta: Delta): DeltaID | null {
|
static getNegatedDeltaId(negationDelta: Delta): DeltaID | null {
|
||||||
const negationPointer = negationDelta.pointers.find(pointer =>
|
const negationPointer = negationDelta.pointers.find(pointer =>
|
||||||
pointer.localContext === 'negates' &&
|
pointer.localContext === '_negates' &&
|
||||||
pointer.targetContext === 'negated_by'
|
pointer.targetContext === 'negated_by'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,10 +60,10 @@ export class NegationHelper {
|
|||||||
/**
|
/**
|
||||||
* Find all negation deltas that negate a specific delta
|
* Find all negation deltas that negate a specific delta
|
||||||
*/
|
*/
|
||||||
static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): NegationDelta[] {
|
static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): Delta[] {
|
||||||
return deltas
|
return deltas
|
||||||
.filter(delta => this.isNegationDelta(delta))
|
.filter(delta => this.isNegationDelta(delta))
|
||||||
.filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId) as NegationDelta[];
|
.filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +139,7 @@ export class NegationHelper {
|
|||||||
// Create a map of delta ID to its negation status
|
// Create a map of delta ID to its negation status
|
||||||
const deltaStatus = new Map<DeltaID, boolean>();
|
const deltaStatus = new Map<DeltaID, boolean>();
|
||||||
// Create a map of delta ID to its negation deltas
|
// Create a map of delta ID to its negation deltas
|
||||||
const deltaToNegations = new Map<DeltaID, NegationDelta[]>();
|
const deltaToNegations = new Map<DeltaID, Delta[]>();
|
||||||
|
|
||||||
// First pass: collect all deltas and their negations
|
// First pass: collect all deltas and their negations
|
||||||
for (const delta of deltas) {
|
for (const delta of deltas) {
|
||||||
|
@ -63,16 +63,11 @@ export class ImageManager implements IImageManager {
|
|||||||
debug('Created build context tar stream');
|
debug('Created build context tar stream');
|
||||||
|
|
||||||
testImageBuildPromise = new Promise<void>((resolve, reject) => {
|
testImageBuildPromise = new Promise<void>((resolve, reject) => {
|
||||||
const logMessages: string[] = [];
|
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
const log = (...args: any[]) => {
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const message = args.map(arg =>
|
const message = args.map(arg =>
|
||||||
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||||
).join(' ');
|
).join(' ');
|
||||||
const logMessage = `[${timestamp}] ${message}\n`;
|
debug(message);
|
||||||
process.stdout.write(logMessage);
|
|
||||||
logMessages.push(logMessage);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => {
|
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => {
|
||||||
@ -143,17 +138,4 @@ export class ImageManager implements IImageManager {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an image exists locally
|
|
||||||
*/
|
|
||||||
async imageExists(imageName: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const image = this.docker.getImage(imageName);
|
|
||||||
await image.inspect();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -268,9 +268,13 @@ export class QueryEngine {
|
|||||||
switch (propertySchema.type) {
|
switch (propertySchema.type) {
|
||||||
case 'primitive': {
|
case 'primitive': {
|
||||||
// Use last-write-wins for primitives
|
// Use last-write-wins for primitives
|
||||||
const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0];
|
const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
|
||||||
const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId);
|
for (let delta of deltasSorted) {
|
||||||
|
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
|
||||||
|
if (primitiveValue !== null) {
|
||||||
obj[propertyId] = primitiveValue;
|
obj[propertyId] = primitiveValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,12 +308,12 @@ export class QueryEngine {
|
|||||||
/**
|
/**
|
||||||
* Extract primitive value from a delta for a given property
|
* Extract primitive value from a delta for a given property
|
||||||
*/
|
*/
|
||||||
private extractPrimitiveValue(delta: CollapsedDelta, _propertyId: string): unknown {
|
private extractPrimitiveValue(delta: CollapsedDelta, propertyId: string): unknown {
|
||||||
// Look for the value in collapsed pointers
|
// Look for the value in collapsed pointers
|
||||||
// CollapsedPointer is {[key: PropertyID]: PropertyTypes}
|
// CollapsedPointer is {[key: PropertyID]: PropertyTypes}
|
||||||
for (const pointer of delta.pointers) {
|
for (const pointer of delta.pointers) {
|
||||||
if (pointer.value !== undefined) {
|
if (pointer[propertyId] !== undefined) {
|
||||||
return pointer.value;
|
return pointer[propertyId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -263,6 +263,8 @@ export class StorageQueryEngine {
|
|||||||
default:
|
default:
|
||||||
properties[propertyId] = propDeltas.length;
|
properties[propertyId] = propDeltas.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(`Resolved property ${propertyId}:`, properties[propertyId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
@ -271,9 +273,9 @@ export class StorageQueryEngine {
|
|||||||
/**
|
/**
|
||||||
* Extract primitive value from a delta for a given property
|
* Extract primitive value from a delta for a given property
|
||||||
*/
|
*/
|
||||||
private extractPrimitiveValue(delta: Delta, _propertyId: string): unknown {
|
private extractPrimitiveValue(delta: Delta, propertyId: string): unknown {
|
||||||
for (const pointer of delta.pointers) {
|
for (const pointer of delta.pointers) {
|
||||||
if (pointer.localContext === 'value') {
|
if (pointer.localContext === propertyId) {
|
||||||
return pointer.target;
|
return pointer.target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,11 +283,11 @@ export class StorageQueryEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract reference value (target ID) from a delta for a given property
|
* Extract reference value from a delta for a given property
|
||||||
*/
|
*/
|
||||||
private extractReferenceValue(delta: Delta, _propertyId: string): string | null {
|
private extractReferenceValue(delta: Delta, propertyId: string): string | null {
|
||||||
for (const pointer of delta.pointers) {
|
for (const pointer of delta.pointers) {
|
||||||
if (pointer.localContext === 'value' && typeof pointer.target === 'string') {
|
if (pointer.localContext === propertyId && typeof pointer.target === 'string') {
|
||||||
return pointer.target;
|
return pointer.target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
|
|
||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from '../core/delta';
|
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1, DeltaV2} from '../core/delta';
|
||||||
import {RhizomeNode} from '../node';
|
import {RhizomeNode} from '../node';
|
||||||
import {Transactions} from '../features/transactions';
|
import {Transactions} from '../features/transactions';
|
||||||
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "../core/types";
|
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "../core/types";
|
||||||
import {Negation} from '../features/negation';
|
import {Negation} from '../features/negation';
|
||||||
import {NegationHelper} from '../features/negation';
|
import {NegationHelper} from '../features/negation';
|
||||||
|
import { createDelta } from '../core/delta-builder';
|
||||||
const debug = Debug('rz:lossless');
|
const debug = Debug('rz:lossless');
|
||||||
|
|
||||||
export type CollapsedPointer = {[key: PropertyID]: PropertyTypes};
|
export type CollapsedPointer = {[key: PropertyID]: PropertyTypes};
|
||||||
@ -34,7 +35,11 @@ class LosslessEntity {
|
|||||||
|
|
||||||
constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {}
|
constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {}
|
||||||
|
|
||||||
addDelta(delta: Delta) {
|
addDelta(delta: Delta | DeltaV2) {
|
||||||
|
// Convert DeltaV2 to DeltaV1 if needed
|
||||||
|
if (delta instanceof DeltaV2) {
|
||||||
|
delta = delta.toV1();
|
||||||
|
}
|
||||||
const targetContexts = delta.pointers
|
const targetContexts = delta.pointers
|
||||||
.filter(({target}) => target === this.id)
|
.filter(({target}) => target === this.id)
|
||||||
.map(({targetContext}) => targetContext)
|
.map(({targetContext}) => targetContext)
|
||||||
@ -87,7 +92,12 @@ export class Lossless {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ingestDelta(delta: Delta): TransactionID | undefined {
|
ingestDelta(delta: Delta | DeltaV2): TransactionID | undefined {
|
||||||
|
// Convert DeltaV2 to DeltaV1 if needed
|
||||||
|
if (delta instanceof DeltaV2) {
|
||||||
|
delta = delta.toV1();
|
||||||
|
}
|
||||||
|
|
||||||
// Store delta for negation processing
|
// Store delta for negation processing
|
||||||
this.allDeltas.set(delta.id, delta);
|
this.allDeltas.set(delta.id, delta);
|
||||||
|
|
||||||
@ -114,10 +124,10 @@ export class Lossless {
|
|||||||
// Add negation delta to the entity
|
// Add negation delta to the entity
|
||||||
// For negation deltas, we need to add them to a special property
|
// For negation deltas, we need to add them to a special property
|
||||||
// since they don't directly target the entity
|
// since they don't directly target the entity
|
||||||
let negationDeltas = ent.properties.get('_negations');
|
let negationDeltas = ent.properties.get('_negates');
|
||||||
if (!negationDeltas) {
|
if (!negationDeltas) {
|
||||||
negationDeltas = new Set<Delta>();
|
negationDeltas = new Set<Delta>();
|
||||||
ent.properties.set('_negations', negationDeltas);
|
ent.properties.set('_negates', negationDeltas);
|
||||||
}
|
}
|
||||||
negationDeltas.add(delta);
|
negationDeltas.add(delta);
|
||||||
}
|
}
|
||||||
@ -190,31 +200,35 @@ export class Lossless {
|
|||||||
for (const delta of deltas) {
|
for (const delta of deltas) {
|
||||||
if (!seenDeltaIds.has(delta.id)) {
|
if (!seenDeltaIds.has(delta.id)) {
|
||||||
seenDeltaIds.add(delta.id);
|
seenDeltaIds.add(delta.id);
|
||||||
// Convert CollapsedDelta back to Delta
|
|
||||||
const fullDelta = new Delta({
|
// Create a new delta using DeltaBuilder
|
||||||
id: delta.id,
|
const builder = createDelta(delta.creator, delta.host)
|
||||||
creator: delta.creator,
|
.withId(delta.id)
|
||||||
host: delta.host,
|
.withTimestamp(delta.timeCreated);
|
||||||
timeCreated: delta.timeCreated,
|
|
||||||
pointers: delta.pointers.map(pointer => {
|
// Add all pointers from the collapsed delta
|
||||||
// Convert back to V1 pointer format for Delta constructor
|
for (const pointer of delta.pointers) {
|
||||||
const pointerEntries = Object.entries(pointer);
|
const pointerEntries = Object.entries(pointer);
|
||||||
if (pointerEntries.length === 1) {
|
if (pointerEntries.length === 1) {
|
||||||
const [localContext, target] = pointerEntries[0];
|
const [localContext, target] = pointerEntries[0];
|
||||||
|
if (target === null || target === undefined) {
|
||||||
|
continue; // Skip null/undefined targets
|
||||||
|
}
|
||||||
if (typeof target === 'string' && this.domainEntities.has(target)) {
|
if (typeof target === 'string' && this.domainEntities.has(target)) {
|
||||||
// This is a reference pointer to an entity
|
// This is a reference pointer to an entity
|
||||||
// The targetContext is the property ID this delta appears under
|
builder.addPointer(localContext, target, propertyId);
|
||||||
return { localContext, target, targetContext: propertyId };
|
} else if (typeof target === 'string' || typeof target === 'number' || typeof target === 'boolean') {
|
||||||
|
// Scalar pointer with valid type
|
||||||
|
builder.addPointer(localContext, target);
|
||||||
} else {
|
} else {
|
||||||
// Scalar pointer
|
// For other types (objects, arrays), convert to string
|
||||||
return { localContext, target: target as PropertyTypes };
|
builder.addPointer(localContext, JSON.stringify(target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback for unexpected pointer structure
|
}
|
||||||
return { localContext: 'unknown', target: 'unknown' };
|
|
||||||
})
|
// Build the delta and add to results
|
||||||
});
|
allDeltas.push(builder.buildV1());
|
||||||
allDeltas.push(fullDelta);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -349,8 +363,8 @@ export class Lossless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [property, deltas] of ent.properties.entries()) {
|
for (const [property, deltas] of ent.properties.entries()) {
|
||||||
// Skip the special _negations property in the per-property stats
|
// Skip the special _negates property in the per-property stats
|
||||||
if (property === '_negations') {
|
if (property === '_negates') {
|
||||||
totalDeltas += deltas.size;
|
totalDeltas += deltas.size;
|
||||||
totalNegationDeltas += deltas.size;
|
totalNegationDeltas += deltas.size;
|
||||||
continue;
|
continue;
|
||||||
@ -384,7 +398,7 @@ export class Lossless {
|
|||||||
const ent = this.domainEntities.get(entityId);
|
const ent = this.domainEntities.get(entityId);
|
||||||
if (!ent) return [];
|
if (!ent) return [];
|
||||||
|
|
||||||
const negationProperty = ent.properties.get('_negations');
|
const negationProperty = ent.properties.get('_negates');
|
||||||
if (!negationProperty) return [];
|
if (!negationProperty) return [];
|
||||||
|
|
||||||
return Array.from(negationProperty);
|
return Array.from(negationProperty);
|
||||||
|
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