refactor: simplify DeltaBuilder and update tests

- Removed V1-specific code paths in favor of V2 with toV1() conversion
- Simplified pointer handling to use a single internal representation
- Made entityLabel configurable in setProperty()
- Updated tests to use buildV1() and buildV2() explicitly
- Removed unused time utility module
- Added more comprehensive test coverage for entity properties

This change makes the DeltaBuilder more maintainable by reducing code duplication and following the pattern of building V2 deltas first, then converting to V1 when needed.
This commit is contained in:
Lentil Hoffman 2025-06-20 10:40:31 -05:00
parent 3ca8249510
commit 8043b67258
3 changed files with 67 additions and 157 deletions

View File

@ -1,4 +1,4 @@
import { createDelta, DeltaBuilder } from '../src/core/delta-builder'; import { createDelta } from '../src/core/delta-builder';
import { DeltaV1, DeltaV2 } from '../src/core/delta'; import { DeltaV1, DeltaV2 } from '../src/core/delta';
import { Lossless } from '../src/views/lossless'; import { Lossless } from '../src/views/lossless';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
@ -11,11 +11,10 @@ describe('DeltaBuilder', () => {
describe('V1 Deltas', () => { describe('V1 Deltas', () => {
it('should create a basic V1 delta', () => { it('should create a basic V1 delta', () => {
const builder = new DeltaBuilder(creator, host, 'v1'); const delta = createDelta(creator, host)
const delta = builder
.addPointer('name', 'Test Delta', 'title') .addPointer('name', 'Test Delta', 'title')
.addPointer('description', 'A test delta', 'description') .addPointer('description', 'A test delta', 'description')
.build(); .buildV1();
expect(delta).toBeInstanceOf(DeltaV1); expect(delta).toBeInstanceOf(DeltaV1);
expect(delta.id).toBeDefined(); expect(delta.id).toBeDefined();
@ -29,22 +28,46 @@ describe('DeltaBuilder', () => {
}); });
}); });
it.only('should create a V1 delta with setProperty', () => { it('should create a V1 delta with setProperty', () => {
const delta = createDelta(creator, host, 'v1') const delta = createDelta(creator, host)
.setProperty('entity-1', 'name', 'Test Entity') .setProperty('entity-1', 'name', 'Test Entity')
.build(); .buildV1();
expect(delta).toBeInstanceOf(DeltaV1); expect(delta).toBeInstanceOf(DeltaV1);
expect(delta.pointers).toContainEqual({
localContext: 'name',
target: 'Test Entity',
targetContext: 'name'
});
expect(delta.pointers).toContainEqual({ expect(delta.pointers).toContainEqual({
localContext: 'entity', localContext: 'entity',
target: 'entity-1', target: 'entity-1',
targetContext: 'name' 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 // Verify that the entity property resolves correctly
const lossless = new Lossless(node); const lossless = new Lossless(node);
@ -56,9 +79,9 @@ describe('DeltaBuilder', () => {
}); });
it('should create a V1 delta with relationships', () => { it('should create a V1 delta with relationships', () => {
const delta = createDelta(creator, host, 'v1') const delta = createDelta(creator, host)
.relate('user-1', 'follows', 'user-2') .relate('user-1', 'follows', 'user-2')
.build(); .buildV1();
expect(delta.pointers).toContainEqual({ expect(delta.pointers).toContainEqual({
localContext: 'follows', localContext: 'follows',
@ -75,8 +98,7 @@ describe('DeltaBuilder', () => {
describe('V2 Deltas', () => { describe('V2 Deltas', () => {
it('should create a basic V2 delta', () => { it('should create a basic V2 delta', () => {
const builder = new DeltaBuilder(creator, host, 'v2'); const delta = createDelta(creator, host)
const delta = builder
.addPointer('name', 'Test Delta V2', 'title') .addPointer('name', 'Test Delta V2', 'title')
.buildV2(); .buildV2();
@ -89,7 +111,7 @@ describe('DeltaBuilder', () => {
}); });
it('should create a V2 delta with setProperty', () => { it('should create a V2 delta with setProperty', () => {
const delta = createDelta(creator, host, 'v2') const delta = createDelta(creator, host)
.setProperty('entity-1', 'name', 'Test Entity') .setProperty('entity-1', 'name', 'Test Entity')
.buildV2(); .buildV2();
@ -98,7 +120,7 @@ describe('DeltaBuilder', () => {
}); });
it('should create a V2 delta with relationships', () => { it('should create a V2 delta with relationships', () => {
const delta = createDelta(creator, host, 'v2') const delta = createDelta(creator, host)
.relate('user-1', 'follows', 'user-2') .relate('user-1', 'follows', 'user-2')
.buildV2(); .buildV2();
@ -112,7 +134,7 @@ describe('DeltaBuilder', () => {
const customId = 'custom-delta-id'; const customId = 'custom-delta-id';
const delta = createDelta(creator, host) const delta = createDelta(creator, host)
.withId(customId) .withId(customId)
.build(); .buildV1();
expect(delta.id).toBe(customId); expect(delta.id).toBe(customId);
}); });
@ -121,7 +143,7 @@ describe('DeltaBuilder', () => {
const txId = 'tx-123'; const txId = 'tx-123';
const delta = createDelta(creator, host) const delta = createDelta(creator, host)
.inTransaction(txId) .inTransaction(txId)
.build(); .buildV1();
// Check for transaction ID in pointers // Check for transaction ID in pointers
const txPointer = delta.pointers.find(p => p.localContext === '_transaction'); const txPointer = delta.pointers.find(p => p.localContext === '_transaction');
@ -131,7 +153,7 @@ describe('DeltaBuilder', () => {
it('should support transactions in V2', () => { it('should support transactions in V2', () => {
const txId = 'tx-123'; const txId = 'tx-123';
const delta = createDelta(creator, host, 'v2') const delta = createDelta(creator, host)
.inTransaction(txId) .inTransaction(txId)
.buildV2(); .buildV2();
@ -143,7 +165,7 @@ describe('DeltaBuilder', () => {
const negatedId = 'delta-to-negate'; const negatedId = 'delta-to-negate';
const delta = createDelta(creator, host) const delta = createDelta(creator, host)
.negate(negatedId) .negate(negatedId)
.build(); .buildV1();
// Check for negation in pointers // Check for negation in pointers
const negationPointer = delta.pointers.find(p => p.localContext === '_negation'); const negationPointer = delta.pointers.find(p => p.localContext === '_negation');
@ -155,7 +177,7 @@ describe('DeltaBuilder', () => {
const timestamp = Date.now(); const timestamp = Date.now();
const delta = createDelta(creator, host) const delta = createDelta(creator, host)
.withTimestamp(timestamp) .withTimestamp(timestamp)
.build(); .buildV1();
expect(delta.timeCreated).toBe(timestamp); expect(delta.timeCreated).toBe(timestamp);
}); });

View File

@ -1,24 +1,7 @@
import { import { DeltaV1, DeltaV2 } from './delta';
DeltaID,
Delta,
DeltaV1,
DeltaV2,
DeltaNetworkImageV1,
DeltaNetworkImageV2,
PointerTarget,
PointersV2
} from './delta';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { microtime } from '../utils/time'; import Debug from 'debug';
const debug = Debug('rz:delta-builder');
type DeltaVersion = 'v1' | 'v2';
// Local type for V1 pointers
interface PointerV1 {
localContext: string;
target: PointerTarget;
targetContext?: string;
}
/** /**
* A fluent builder for creating Delta objects with proper validation and type safety. * A fluent builder for creating Delta objects with proper validation and type safety.
@ -26,12 +9,10 @@ interface PointerV1 {
*/ */
export class DeltaBuilder { export class DeltaBuilder {
private id: string; private id: string;
private timeCreated: number; private timeCreated?: number;
private host: string; private host: string;
private creator: string; private creator: string;
private version: DeltaVersion = 'v2'; // Default to V2 private pointers: Record<string, any> = {};
private pointersV1: Array<{ localContext: string; target: PointerTarget; targetContext?: string }> = [];
private pointersV2: Record<string, any> = {};
private transactionId?: string; private transactionId?: string;
private isNegation: boolean = false; private isNegation: boolean = false;
private negatedDeltaId?: string; private negatedDeltaId?: string;
@ -40,14 +21,11 @@ export class DeltaBuilder {
* Create a new DeltaBuilder instance * Create a new DeltaBuilder instance
* @param creator - The ID of the entity creating this delta * @param creator - The ID of the entity creating this delta
* @param host - The host where this delta is being created * @param host - The host where this delta is being created
* @param version - The delta version to use ('v1' or 'v2')
*/ */
constructor(creator: string, host: string, version: DeltaVersion = 'v2') { constructor(creator: string, host: string) {
this.id = randomUUID(); this.id = randomUUID();
this.timeCreated = microtime.now();
this.creator = creator; this.creator = creator;
this.host = host; this.host = host;
this.version = version;
} }
/** /**
@ -84,50 +62,23 @@ export class DeltaBuilder {
} }
/** /**
* Add a pointer to the delta (V1 style) * Add a pointer to the delta
*/ */
addPointer(localContext: string, target: string | number | boolean, targetContext?: string): this { addPointer(localContext: string, target: string | number | boolean, targetContext?: string): this {
if (this.version === 'v1') { if (targetContext && typeof target === 'string') {
this.pointersV1.push({ localContext, target, targetContext }); this.pointers[localContext] = { [target]: targetContext };
} else { } else {
// For V2, we need to handle the target context differently this.pointers[localContext] = target;
if (targetContext && typeof target === 'string') {
this.pointersV2[localContext] = { [target]: targetContext };
} else {
this.pointersV2[localContext] = target;
}
} }
return this; return this;
} }
/** /**
* Set a property on an entity (shorthand for addPointer with 'value' local context) * Set a property on an entity
*/ */
setProperty(entityId: string, property: string, value: string | number | boolean, targetContext?: string): this { setProperty(entityId: string, property: string, value: string | number | boolean, entityLabel = "entity"): this {
if (this.version === 'v1') { this.addPointer(entityLabel, entityId, property)
// For V1, we need to ensure target is a valid type this.addPointer(property, value);
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
this.pointersV1.push({
localContext: property,
target: value, // We've checked it's a valid type
targetContext: property
});
// Add a reference to the entity
this.pointersV1.push({
localContext: 'entity',
target: entityId,
targetContext: property
});
}
} else {
// V2 format
if (targetContext) {
this.pointersV2[property] = { [String(value)]: targetContext };
} else {
this.pointersV2[property] = value;
}
this.pointersV2.entity = { [entityId]: property };
}
return this; return this;
} }
@ -135,41 +86,17 @@ export class DeltaBuilder {
* Create a relationship between two entities * Create a relationship between two entities
*/ */
relate(sourceId: string, relationship: string, targetId: string): this { relate(sourceId: string, relationship: string, targetId: string): this {
if (this.version === 'v1') { this.pointers[relationship] = { [targetId]: relationship };
this.pointersV1.push({ this.pointers.source = { [sourceId]: relationship };
localContext: relationship,
target: targetId,
targetContext: relationship
});
this.pointersV1.push({
localContext: 'source',
target: sourceId,
targetContext: relationship
});
} else {
this.pointersV2[relationship] = { [targetId]: relationship };
this.pointersV2.source = { [sourceId]: relationship };
}
return this; return this;
} }
/**
* Build and return a Delta instance
*/
build(): Delta {
if (this.version === 'v1') {
return this.buildV1();
} else {
return this.buildV2().toV1();
}
}
/** /**
* Build and return a DeltaV2 instance * Build and return a DeltaV2 instance
*/ */
buildV2(): DeltaV2 { buildV2(): DeltaV2 {
// For V2, we'll store transaction and negation info in the pointers object // For V2, we'll store transaction and negation info in the pointers object
const pointers = { ...this.pointersV2 }; const pointers = { ...this.pointers };
if (this.transactionId) { if (this.transactionId) {
pointers['_transaction'] = this.transactionId; pointers['_transaction'] = this.transactionId;
@ -182,9 +109,9 @@ export class DeltaBuilder {
// Create the delta with all pointers // Create the delta with all pointers
return new DeltaV2({ return new DeltaV2({
id: this.id, id: this.id,
timeCreated: this.timeCreated,
host: this.host, host: this.host,
creator: this.creator, creator: this.creator,
timeCreated: this.timeCreated,
pointers pointers
}); });
} }
@ -192,38 +119,14 @@ export class DeltaBuilder {
/** /**
* Build and return a DeltaV1 instance * Build and return a DeltaV1 instance
*/ */
private buildV1(): DeltaV1 { buildV1(): DeltaV1 {
// For V1, we'll store transaction and negation info in the pointers return this.buildV2().toV1();
const pointers = [...this.pointersV1];
if (this.transactionId) {
pointers.push({
localContext: '_transaction',
target: this.transactionId
});
}
if (this.isNegation && this.negatedDeltaId) {
pointers.push({
localContext: '_negation',
target: this.negatedDeltaId
});
}
// Create the delta with all pointers
return new DeltaV1({
id: this.id,
timeCreated: this.timeCreated,
host: this.host,
creator: this.creator,
pointers
});
} }
} }
/** /**
* Create a new DeltaBuilder instance (convenience function) * Create a new DeltaBuilder instance (convenience function)
*/ */
export function createDelta(creator: string, host: string, version: DeltaVersion = 'v2'): DeltaBuilder { export function createDelta(creator: string, host: string): DeltaBuilder {
return new DeltaBuilder(creator, host, version); return new DeltaBuilder(creator, host);
} }

View File

@ -1,15 +0,0 @@
/**
* Microsecond-precision timestamp utilities
*/
/**
* Get current time in microseconds since epoch
*/
export function microtimeNow(): number {
const [seconds, nanoseconds] = process.hrtime();
return Math.floor(seconds * 1e6 + nanoseconds / 1e3);
}
export const microtime = {
now: microtimeNow
};