Delta Builder Fluent API #4

Merged
lentil merged 7 commits from chore/delta-format-standardization into main 2025-06-20 22:55:33 -05:00
3 changed files with 67 additions and 157 deletions
Showing only changes of commit 8043b67258 - Show all commits

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') {
this.pointersV1.push({ localContext, target, targetContext });
} else {
// For V2, we need to handle the target context differently
if (targetContext && typeof target === 'string') { if (targetContext && typeof target === 'string') {
this.pointersV2[localContext] = { [target]: targetContext }; this.pointers[localContext] = { [target]: targetContext };
} else { } else {
this.pointersV2[localContext] = target; this.pointers[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
};