Compare commits
9 Commits
feature/do
...
main
Author | SHA1 | Date | |
---|---|---|---|
62d0f5355c | |||
795551c623 | |||
60ad920b30 | |||
5c1c8a23b8 | |||
4542c4ce83 | |||
8043b67258 | |||
3ca8249510 | |||
c173f3475e | |||
3a6191a2a2 |
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
|
|
||||||
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
|
await orchestrator.cleanup();
|
||||||
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;
|
||||||
@ -20,8 +20,10 @@ describe('Schema System', () => {
|
|||||||
schemaRegistry = new DefaultSchemaRegistry();
|
schemaRegistry = new DefaultSchemaRegistry();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@ export class QueryEngine {
|
|||||||
// Convert each schema property from lossless view deltas
|
// Convert each schema property from lossless view deltas
|
||||||
for (const [propertyId, propertySchema] of Object.entries(schema.properties)) {
|
for (const [propertyId, propertySchema] of Object.entries(schema.properties)) {
|
||||||
const deltas = view.propertyDeltas[propertyId] || [];
|
const deltas = view.propertyDeltas[propertyId] || [];
|
||||||
|
|
||||||
if (deltas.length === 0) {
|
if (deltas.length === 0) {
|
||||||
obj[propertyId] = null;
|
obj[propertyId] = null;
|
||||||
continue;
|
continue;
|
||||||
@ -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) {
|
||||||
obj[propertyId] = primitiveValue;
|
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
|
||||||
|
if (primitiveValue !== null) {
|
||||||
|
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 (typeof target === 'string' && this.domainEntities.has(target)) {
|
if (target === null || target === undefined) {
|
||||||
// This is a reference pointer to an entity
|
continue; // Skip null/undefined targets
|
||||||
// The targetContext is the property ID this delta appears under
|
|
||||||
return { localContext, target, targetContext: propertyId };
|
|
||||||
} else {
|
|
||||||
// Scalar pointer
|
|
||||||
return { localContext, target: target as PropertyTypes };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Fallback for unexpected pointer structure
|
if (typeof target === 'string' && this.domainEntities.has(target)) {
|
||||||
return { localContext: 'unknown', target: 'unknown' };
|
// This is a reference pointer to an entity
|
||||||
})
|
builder.addPointer(localContext, target, propertyId);
|
||||||
});
|
} else if (typeof target === 'string' || typeof target === 'number' || typeof target === 'boolean') {
|
||||||
allDeltas.push(fullDelta);
|
// Scalar pointer with valid type
|
||||||
|
builder.addPointer(localContext, target);
|
||||||
|
} else {
|
||||||
|
// For other types (objects, arrays), convert to string
|
||||||
|
builder.addPointer(localContext, JSON.stringify(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the delta and add to results
|
||||||
|
allDeltas.push(builder.buildV1());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -349,8 +363,8 @@ export class Lossless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [property, deltas] of ent.properties.entries()) {
|
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