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