refactor: migrate all delta creation to use DeltaBuilder

- Replace all direct  instantiations with
- Implement proper transaction handling in DeltaBuilder
- Update negation system to work with the builder pattern
- Fix type issues with null values in pointers
- Update all tests to work with the new implementation
- Ensure all tests pass with the refactored code

This change improves code consistency and maintainability by using
a single, fluent API for all delta creation throughout the codebase.
This commit is contained in:
Lentil Hoffman 2025-06-20 22:45:10 -05:00
parent 60ad920b30
commit 795551c623
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
9 changed files with 83 additions and 150 deletions

View File

@ -146,9 +146,11 @@ describe('DeltaBuilder', () => {
.buildV1();
// Check for transaction ID in pointers
const txPointer = delta.pointers.find(p => p.localContext === '_transaction');
expect(txPointer).toBeDefined();
expect(txPointer?.target).toBe(txId);
expect(delta.pointers).toContainEqual({
localContext: '_transaction',
target: txId,
targetContext: 'deltas'
});
});
it('should support transactions in V2', () => {
@ -168,7 +170,7 @@ describe('DeltaBuilder', () => {
.buildV1();
// Check for negation in pointers
const negationPointer = delta.pointers.find(p => p.localContext === '_negation');
const negationPointer = delta.pointers.find(p => p.localContext === '_negates');
expect(negationPointer).toBeDefined();
expect(negationPointer?.target).toBe(negatedId);
});

View File

@ -248,8 +248,7 @@ describe('Lossless', () => {
losslessT.ingestDelta(
createDelta('A', 'H')
.inTransaction(transactionId)
.addPointer('step', 'process1', 'status')
.addPointer('value', 'started')
.setProperty('process1', 'status', 'started', 'step')
.buildV1()
);
@ -257,8 +256,7 @@ describe('Lossless', () => {
losslessT.ingestDelta(
createDelta('B', 'H')
.inTransaction(transactionId)
.addPointer('step', 'process1', 'status')
.addPointer('value', 'processing')
.setProperty('process1', 'status', 'processing', 'step')
.buildV1()
);

View File

@ -1,6 +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';
@ -28,15 +27,14 @@ 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', () => {

View File

@ -194,14 +194,9 @@ describe('Nested Object Resolution Performance', () => {
const currentId = userIds[i];
const nextId = userIds[i + 1];
const linkDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: currentId, targetContext: 'next' },
{ localContext: 'next', target: nextId }
]
});
const linkDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(currentId, 'next', nextId, 'users')
.buildV1();
node.lossless.ingestDelta(linkDelta);
}
@ -293,14 +288,10 @@ describe('Nested Object Resolution Performance', () => {
const connectedIndex = (i + j) % userCount;
const connectedId = userIds[connectedIndex];
const connectionDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: userId, targetContext: 'connections' },
{ localContext: 'connections', target: connectedId }
]
});
const connectionDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', userId, 'connections')
.addPointer('connections', connectedId)
.buildV1();
node.lossless.ingestDelta(connectionDelta);
}
}

View File

@ -24,15 +24,13 @@ describe('Transactions', () => {
// Create first delta in transaction
const delta1 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.addPointer('name', 'user123', 'name')
.addPointer('value', 'Alice')
.setProperty('user123', 'name', 'Alice')
.buildV1();
// Create second delta in transaction
const delta2 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.addPointer('age', 'user123', 'age')
.addPointer('value', 25)
.setProperty('user123', 'age', 25)
.buildV1();
// Ingest transaction declaration and first two deltas
@ -47,8 +45,7 @@ describe('Transactions', () => {
// Add the third delta to complete the transaction
const delta3 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.addPointer('email', 'user123', 'email')
.addPointer('value', 'alice@example.com')
.setProperty('user123', 'email', 'alice@example.com')
.buildV1();
lossless.ingestDelta(delta3);
@ -79,15 +76,13 @@ describe('Transactions', () => {
// Add deltas for both transactions
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(tx1)
.addPointer('status', 'order1', 'status')
.addPointer('value', 'pending')
.setProperty('order1', 'status', 'pending')
.buildV1()
);
lossless.ingestDelta(createDelta('user2', 'host2')
.inTransaction(tx2)
.addPointer('status', 'order2', 'status')
.addPointer('value', 'shipped')
.setProperty('order2', 'status', 'shipped')
.buildV1()
);
@ -99,8 +94,7 @@ describe('Transactions', () => {
// Complete tx1
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(tx1)
.addPointer('total', 'order1', 'total')
.addPointer('value', 100)
.setProperty('order1', 'total', 100)
.buildV1()
);
@ -114,8 +108,7 @@ describe('Transactions', () => {
// Complete tx2
lossless.ingestDelta(createDelta('user2', 'host2')
.inTransaction(tx2)
.addPointer('tracking', 'order2', 'tracking')
.addPointer('value', 'TRACK123')
.setProperty('order2', 'tracking', 'TRACK123')
.buildV1()
);
@ -139,15 +132,13 @@ describe('Transactions', () => {
// Add both deltas
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(transactionId)
.addPointer('type', 'doc1', 'type')
.addPointer('value', 'report')
.setProperty('doc1', 'type', 'report')
.buildV1()
);
lossless.ingestDelta(createDelta('user2', 'host2')
.inTransaction(transactionId)
.addPointer('author', 'doc1', 'author')
.addPointer('value', 'Bob')
.setProperty('doc1', 'author', 'Bob')
.buildV1()
);
@ -169,14 +160,13 @@ describe('Transactions', () => {
// Transaction that updates multiple entities atomically
lossless.ingestDelta(createDelta('system', 'host1')
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', 3)
.declareTransaction(transactionId, 3)
.buildV1()
);
// Transfer money from account1 to account2
lossless.ingestDelta(createDelta('bank', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.inTransaction(transactionId)
.addPointer('balance', 'account1', 'balance')
.addPointer('value', 900)
.addPointer('operation', 'debit')
@ -184,7 +174,7 @@ describe('Transactions', () => {
);
lossless.ingestDelta(createDelta('bank', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.inTransaction(transactionId)
.addPointer('balance', 'account2', 'balance')
.addPointer('value', 1100)
.addPointer('operation', 'credit')
@ -198,7 +188,7 @@ describe('Transactions', () => {
// Complete transaction with audit log
lossless.ingestDelta(createDelta('bank', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.inTransaction(transactionId)
.addPointer('transfer', 'transfer123', 'details')
.addPointer('from', 'account1')
.addPointer('to', 'account2')
@ -227,16 +217,14 @@ describe('Transactions', () => {
// Create transaction
lossless.ingestDelta(createDelta('system', 'host1')
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', 2)
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add first delta
const delta1 = createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('field1', 'entity1', 'field1')
.addPointer('value', 'value1')
.inTransaction(transactionId)
.setProperty('entity1', 'field1', 'value1')
.buildV1();
lossless.ingestDelta(delta1);
@ -245,9 +233,8 @@ describe('Transactions', () => {
// Add second delta to complete transaction
const delta2 = createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('field2', 'entity1', 'field2')
.addPointer('value', 'value2')
.inTransaction(transactionId)
.setProperty('entity1', 'field2', 'value2')
.buildV1();
lossless.ingestDelta(delta2);
@ -270,16 +257,14 @@ describe('Transactions', () => {
// Create transaction
lossless.ingestDelta(createDelta('system', 'host1')
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', 2)
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add first delta
lossless.ingestDelta(createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('status', 'job1', 'status')
.addPointer('value', 'processing')
.inTransaction(transactionId)
.setProperty('job1', 'status', 'processing')
.buildV1()
);
@ -294,9 +279,8 @@ describe('Transactions', () => {
// Complete transaction
lossless.ingestDelta(createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('status', 'job1', 'status')
.addPointer('value', 'completed')
.inTransaction(transactionId)
.setProperty('job1', 'status', 'completed')
.buildV1()
);
@ -340,21 +324,20 @@ describe('Transactions', () => {
// Initially declare transaction with size 2
lossless.ingestDelta(createDelta('system', 'host1')
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', 2)
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add 2 deltas
lossless.ingestDelta(createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('item1', 'cart1', 'items')
.inTransaction(transactionId)
.setProperty('cart1', 'items', 'item1')
.buildV1()
);
lossless.ingestDelta(createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('item2', 'cart1', 'items')
.inTransaction(transactionId)
.setProperty('cart1', 'items', 'item2')
.buildV1()
);
@ -371,9 +354,8 @@ describe('Transactions', () => {
// Add delta with transaction reference but no size declaration
lossless.ingestDelta(createDelta('user1', 'host1')
.addPointer('_transaction', transactionId, 'deltas')
.addPointer('data', 'entity1', 'data')
.addPointer('value', 'test')
.inTransaction(transactionId)
.setProperty('entity1', 'data', 'test')
.buildV1()
);
@ -386,8 +368,7 @@ describe('Transactions', () => {
// Declare size after the fact
lossless.ingestDelta(createDelta('system', 'host1')
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', 1)
.declareTransaction(transactionId, 1)
.buildV1()
);

View File

@ -2,6 +2,7 @@ import Debug from 'debug';
import {randomUUID} from "node:crypto";
import EventEmitter from "node:events";
import {Delta} from "../core/delta";
import {createDelta} from "../core/delta-builder";
import {Entity, EntityProperties} from "../core/entity";
import {ResolvedViewOne} from '../views/resolvers/last-write-wins';
import {RhizomeNode} from "../node";
@ -72,18 +73,11 @@ export abstract class Collection<View> {
if (key === 'id') return;
if (oldProperties[key] !== value && host && creator) {
deltas.push(new Delta({
creator,
host,
pointers: [{
localContext: this.name,
target: entityId,
targetContext: key
}, {
localContext: key,
target: value
}]
}));
deltas.push(
createDelta(creator, host)
.setProperty(entityId, key, value, this.name)
.buildV1()
);
}
});
@ -91,18 +85,10 @@ export abstract class Collection<View> {
if (deltas.length > 1) {
// We can generate a separate delta describing this transaction
transactionDelta = new Delta({
creator,
host,
pointers: [{
localContext: "_transaction",
target: transactionId,
targetContext: "size"
}, {
localContext: "size",
target: deltas.length
}]
});
transactionDelta = createDelta(creator, host)
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', deltas.length)
.buildV1();
// Also need to annotate the deltas with the transactionId
for (const delta of deltas) {

View File

@ -13,9 +13,6 @@ export class DeltaBuilder {
private host: string;
private creator: string;
private pointers: Record<string, any> = {};
private transactionId?: string;
private isNegation: boolean = false;
private negatedDeltaId?: string;
/**
* Create a new DeltaBuilder instance
@ -48,7 +45,7 @@ export class DeltaBuilder {
* Set the transaction ID for this delta
*/
inTransaction(transactionId: string): this {
this.transactionId = transactionId;
this.addPointer('_transaction', transactionId, 'deltas');
return this;
}
@ -59,8 +56,7 @@ export class DeltaBuilder {
* @returns
*/
declareTransaction(transactionId: string, size: number): this {
this.addPointer('_transaction', transactionId, 'size');
this.addPointer('size', size)
this.setProperty(transactionId, 'size', size, '_transaction');
return this;
}
@ -68,15 +64,17 @@ export class DeltaBuilder {
* Mark this delta as a negation of another delta
*/
negate(deltaId: string): this {
this.isNegation = true;
this.negatedDeltaId = deltaId;
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, targetContext?: string): this {
addPointer(localContext: string, target: string | number | boolean | null, targetContext?: string): this {
if (targetContext && typeof target === 'string') {
this.pointers[localContext] = { [target]: targetContext };
} else {
@ -88,7 +86,7 @@ export class DeltaBuilder {
/**
* Set a property on an entity
*/
setProperty(entityId: string, property: string, value: string | number | boolean, entityLabel = "entity"): this {
setProperty(entityId: string, property: string, value: string | number | boolean | null, entityLabel = "entity"): this {
this.addPointer(entityLabel, entityId, property)
this.addPointer(property, value);
return this;
@ -110,14 +108,6 @@ export class DeltaBuilder {
// For V2, we'll store transaction and negation info in the pointers object
const pointers = { ...this.pointers };
if (this.transactionId) {
pointers['_transaction'] = { [this.transactionId]: 'deltas' };
}
if (this.isNegation && this.negatedDeltaId) {
pointers['_negation'] = this.negatedDeltaId;
}
// Create the delta with all pointers
return new DeltaV2({
id: this.id,

View File

@ -1,21 +1,17 @@
import Debug from 'debug';
import { Delta, DeltaID } from '../core/delta';
import { createDelta } from '../core/delta-builder';
import { CreatorID, HostID } from '../core/types';
const debug = Debug('rz:negation');
// Negation-specific types
export interface NegationPointer {
localContext: 'negates';
localContext: '_negates';
target: DeltaID;
targetContext: 'negated_by';
}
export interface NegationDelta extends Delta {
isNegation: true;
negatedDeltaId: DeltaID;
}
// Helper functions for creating and identifying negation deltas
export class NegationHelper {
@ -26,19 +22,10 @@ export class NegationHelper {
deltaToNegate: DeltaID,
creator: CreatorID,
host: HostID
): NegationDelta {
const negationDelta = new Delta({
creator,
host,
pointers: [{
localContext: 'negates',
target: deltaToNegate,
targetContext: 'negated_by'
}]
}) as NegationDelta;
negationDelta.isNegation = true;
negationDelta.negatedDeltaId = deltaToNegate;
): Delta {
const negationDelta = createDelta(creator, host)
.negate(deltaToNegate)
.buildV1();
debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`);
return negationDelta;
@ -47,9 +34,9 @@ export class NegationHelper {
/**
* Check if a delta is a negation delta
*/
static isNegationDelta(delta: Delta): delta is NegationDelta {
static isNegationDelta(delta: Delta): boolean {
return delta.pointers.some(pointer =>
pointer.localContext === 'negates' &&
pointer.localContext === '_negates' &&
pointer.targetContext === 'negated_by'
);
}
@ -59,7 +46,7 @@ export class NegationHelper {
*/
static getNegatedDeltaId(negationDelta: Delta): DeltaID | null {
const negationPointer = negationDelta.pointers.find(pointer =>
pointer.localContext === 'negates' &&
pointer.localContext === '_negates' &&
pointer.targetContext === 'negated_by'
);
@ -73,10 +60,10 @@ export class NegationHelper {
/**
* Find all negation deltas that negate a specific delta
*/
static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): NegationDelta[] {
static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): Delta[] {
return deltas
.filter(delta => this.isNegationDelta(delta))
.filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId) as NegationDelta[];
.filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId);
}
/**
@ -152,7 +139,7 @@ export class NegationHelper {
// Create a map of delta ID to its negation status
const deltaStatus = new Map<DeltaID, boolean>();
// Create a map of delta ID to its negation deltas
const deltaToNegations = new Map<DeltaID, NegationDelta[]>();
const deltaToNegations = new Map<DeltaID, Delta[]>();
// First pass: collect all deltas and their negations
for (const delta of deltas) {

View File

@ -124,10 +124,10 @@ export class Lossless {
// Add negation delta to the entity
// For negation deltas, we need to add them to a special property
// since they don't directly target the entity
let negationDeltas = ent.properties.get('_negations');
let negationDeltas = ent.properties.get('_negates');
if (!negationDeltas) {
negationDeltas = new Set<Delta>();
ent.properties.set('_negations', negationDeltas);
ent.properties.set('_negates', negationDeltas);
}
negationDeltas.add(delta);
}
@ -363,8 +363,8 @@ export class Lossless {
}
for (const [property, deltas] of ent.properties.entries()) {
// Skip the special _negations property in the per-property stats
if (property === '_negations') {
// Skip the special _negates property in the per-property stats
if (property === '_negates') {
totalDeltas += deltas.size;
totalNegationDeltas += deltas.size;
continue;
@ -398,7 +398,7 @@ export class Lossless {
const ent = this.domainEntities.get(entityId);
if (!ent) return [];
const negationProperty = ent.properties.get('_negations');
const negationProperty = ent.properties.get('_negates');
if (!negationProperty) return [];
return Array.from(negationProperty);