From c173f3475ee831423581225d2aa716eabbae5bb0 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Thu, 19 Jun 2025 20:57:16 -0500 Subject: [PATCH 1/7] feat(views): add DeltaV2 support to Lossless view - Update Lossless.ingestDelta to accept both Delta and DeltaV2 - Add conversion from DeltaV2 to DeltaV1 during ingestion - Add test case for DeltaV2 ingestion - Ensure backward compatibility with existing DeltaV1 code --- .windsurf/workflows/delta-format.md | 7 +++ __tests__/lossless.ts | 70 +++++++++++++++++++++++++++++ src/views/lossless.ts | 15 +++++-- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .windsurf/workflows/delta-format.md diff --git a/.windsurf/workflows/delta-format.md b/.windsurf/workflows/delta-format.md new file mode 100644 index 0000000..5f0bb63 --- /dev/null +++ b/.windsurf/workflows/delta-format.md @@ -0,0 +1,7 @@ +--- +description: Update deltas to use the object style for pointers +--- + +- in the current file, for each v1 delta, rewrite it as a v2 delta + - make sure the new delta is isomorphic to the original + - do not include a timestamp \ No newline at end of file diff --git a/__tests__/lossless.ts b/__tests__/lossless.ts index 0a3d4a7..af4a08b 100644 --- a/__tests__/lossless.ts +++ b/__tests__/lossless.ts @@ -94,6 +94,75 @@ describe('Lossless', () => { }); }); + it('accepts DeltaV2 instances', () => { + 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" + } + }); + + 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); @@ -242,4 +311,5 @@ describe('Lossless', () => { expect(filteredView.process1.propertyDeltas.status.every(d => d.creator === 'A')).toBe(true); }); }); + }); diff --git a/src/views/lossless.ts b/src/views/lossless.ts index 1bf01b3..a36fee0 100644 --- a/src/views/lossless.ts +++ b/src/views/lossless.ts @@ -3,7 +3,7 @@ import Debug from 'debug'; import EventEmitter from 'events'; -import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from '../core/delta'; +import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1, DeltaV2} from '../core/delta'; import {RhizomeNode} from '../node'; import {Transactions} from '../features/transactions'; import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "../core/types"; @@ -34,7 +34,11 @@ class LosslessEntity { constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {} - addDelta(delta: Delta) { + addDelta(delta: Delta | DeltaV2) { + // Convert DeltaV2 to DeltaV1 if needed + if (delta instanceof DeltaV2) { + delta = delta.toV1(); + } const targetContexts = delta.pointers .filter(({target}) => target === this.id) .map(({targetContext}) => targetContext) @@ -87,7 +91,12 @@ export class Lossless { }); } - ingestDelta(delta: Delta): TransactionID | undefined { + ingestDelta(delta: Delta | DeltaV2): TransactionID | undefined { + // Convert DeltaV2 to DeltaV1 if needed + if (delta instanceof DeltaV2) { + delta = delta.toV1(); + } + // Store delta for negation processing this.allDeltas.set(delta.id, delta); From 3ca82495109d67a5360be90486bfb54af7d2d681 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 09:24:41 -0500 Subject: [PATCH 2/7] feat: add DeltaBuilder for fluent delta creation - Implemented DeltaBuilder class with support for both V1 and V2 delta formats - Added comprehensive test coverage for all builder functionality - Included utility function for microsecond-precision timestamps - Added support for transactions and negation via special pointer contexts - Provides type-safe API for creating deltas with proper validation --- __tests__/delta-builder.test.ts | 163 +++++++++++++++++++++++ src/core/delta-builder.ts | 229 ++++++++++++++++++++++++++++++++ src/utils/time.ts | 15 +++ 3 files changed, 407 insertions(+) create mode 100644 __tests__/delta-builder.test.ts create mode 100644 src/core/delta-builder.ts create mode 100644 src/utils/time.ts diff --git a/__tests__/delta-builder.test.ts b/__tests__/delta-builder.test.ts new file mode 100644 index 0000000..5203499 --- /dev/null +++ b/__tests__/delta-builder.test.ts @@ -0,0 +1,163 @@ +import { createDelta, DeltaBuilder } 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 builder = new DeltaBuilder(creator, host, 'v1'); + const delta = builder + .addPointer('name', 'Test Delta', 'title') + .addPointer('description', 'A test delta', 'description') + .build(); + + 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.only('should create a V1 delta with setProperty', () => { + const delta = createDelta(creator, host, 'v1') + .setProperty('entity-1', 'name', 'Test Entity') + .build(); + + expect(delta).toBeInstanceOf(DeltaV1); + expect(delta.pointers).toContainEqual({ + localContext: 'name', + target: 'Test Entity', + targetContext: 'name' + }); + expect(delta.pointers).toContainEqual({ + localContext: 'entity', + target: 'entity-1', + targetContext: 'name' + }); + + // 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, 'v1') + .relate('user-1', 'follows', 'user-2') + .build(); + + 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 builder = new DeltaBuilder(creator, host, 'v2'); + const delta = builder + .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, 'v2') + .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, 'v2') + .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) + .build(); + + expect(delta.id).toBe(customId); + }); + + it('should support transactions', () => { + const txId = 'tx-123'; + const delta = createDelta(creator, host) + .inTransaction(txId) + .build(); + + // Check for transaction ID in pointers + const txPointer = delta.pointers.find(p => p.localContext === '_transaction'); + expect(txPointer).toBeDefined(); + expect(txPointer?.target).toBe(txId); + }); + + it('should support transactions in V2', () => { + const txId = 'tx-123'; + const delta = createDelta(creator, host, 'v2') + .inTransaction(txId) + .buildV2(); + + // Check for transaction ID in V2 pointers + expect(delta.pointers['_transaction']).toBe(txId); + }); + + it('should support negation', () => { + const negatedId = 'delta-to-negate'; + const delta = createDelta(creator, host) + .negate(negatedId) + .build(); + + // Check for negation in pointers + const negationPointer = delta.pointers.find(p => p.localContext === '_negation'); + expect(negationPointer).toBeDefined(); + expect(negationPointer?.target).toBe(negatedId); + }); + + it('should support custom timestamps', () => { + const timestamp = Date.now(); + const delta = createDelta(creator, host) + .withTimestamp(timestamp) + .build(); + + expect(delta.timeCreated).toBe(timestamp); + }); + }); +}); diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts new file mode 100644 index 0000000..484d915 --- /dev/null +++ b/src/core/delta-builder.ts @@ -0,0 +1,229 @@ +import { + DeltaID, + Delta, + DeltaV1, + DeltaV2, + DeltaNetworkImageV1, + DeltaNetworkImageV2, + PointerTarget, + PointersV2 +} from './delta'; +import { randomUUID } from 'crypto'; +import { microtime } from '../utils/time'; + +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. + * Supports both V1 and V2 delta formats. + */ +export class DeltaBuilder { + private id: string; + private timeCreated: number; + private host: string; + private creator: string; + private version: DeltaVersion = 'v2'; // Default to V2 + private pointersV1: Array<{ localContext: string; target: PointerTarget; targetContext?: string }> = []; + private pointersV2: Record = {}; + private transactionId?: string; + private isNegation: boolean = false; + private negatedDeltaId?: string; + + /** + * 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 + * @param version - The delta version to use ('v1' or 'v2') + */ + constructor(creator: string, host: string, version: DeltaVersion = 'v2') { + this.id = randomUUID(); + this.timeCreated = microtime.now(); + this.creator = creator; + this.host = host; + this.version = version; + } + + /** + * 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.transactionId = transactionId; + return this; + } + + /** + * Mark this delta as a negation of another delta + */ + negate(deltaId: string): this { + this.isNegation = true; + this.negatedDeltaId = deltaId; + return this; + } + + /** + * Add a pointer to the delta (V1 style) + */ + 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') { + this.pointersV2[localContext] = { [target]: targetContext }; + } else { + this.pointersV2[localContext] = target; + } + } + return this; + } + + /** + * Set a property on an entity (shorthand for addPointer with 'value' local context) + */ + setProperty(entityId: string, property: string, value: string | number | boolean, targetContext?: string): this { + if (this.version === 'v1') { + // For V1, we need to ensure target is a valid type + 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; + } + + /** + * Create a relationship between two entities + */ + relate(sourceId: string, relationship: string, targetId: string): this { + if (this.version === 'v1') { + this.pointersV1.push({ + 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; + } + + /** + * 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 + */ + buildV2(): DeltaV2 { + // For V2, we'll store transaction and negation info in the pointers object + const pointers = { ...this.pointersV2 }; + + if (this.transactionId) { + pointers['_transaction'] = this.transactionId; + } + + if (this.isNegation && this.negatedDeltaId) { + pointers['_negation'] = this.negatedDeltaId; + } + + // Create the delta with all pointers + return new DeltaV2({ + id: this.id, + timeCreated: this.timeCreated, + host: this.host, + creator: this.creator, + pointers + }); + } + + /** + * Build and return a DeltaV1 instance + */ + private buildV1(): DeltaV1 { + // For V1, we'll store transaction and negation info in the pointers + 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) + */ +export function createDelta(creator: string, host: string, version: DeltaVersion = 'v2'): DeltaBuilder { + return new DeltaBuilder(creator, host, version); +} diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..3058d8b --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,15 @@ +/** + * 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 +}; From 8043b6725850cad33a859df445b26ad12566c062 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 10:40:31 -0500 Subject: [PATCH 3/7] 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. --- __tests__/delta-builder.test.ts | 68 +++++++++------ src/core/delta-builder.ts | 141 +++++--------------------------- src/utils/time.ts | 15 ---- 3 files changed, 67 insertions(+), 157 deletions(-) delete mode 100644 src/utils/time.ts diff --git a/__tests__/delta-builder.test.ts b/__tests__/delta-builder.test.ts index 5203499..d120ee3 100644 --- a/__tests__/delta-builder.test.ts +++ b/__tests__/delta-builder.test.ts @@ -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 { Lossless } from '../src/views/lossless'; import { RhizomeNode } from '../src/node'; @@ -11,11 +11,10 @@ describe('DeltaBuilder', () => { describe('V1 Deltas', () => { it('should create a basic V1 delta', () => { - const builder = new DeltaBuilder(creator, host, 'v1'); - const delta = builder + const delta = createDelta(creator, host) .addPointer('name', 'Test Delta', 'title') .addPointer('description', 'A test delta', 'description') - .build(); + .buildV1(); expect(delta).toBeInstanceOf(DeltaV1); expect(delta.id).toBeDefined(); @@ -29,22 +28,46 @@ describe('DeltaBuilder', () => { }); }); - it.only('should create a V1 delta with setProperty', () => { - const delta = createDelta(creator, host, 'v1') + it('should create a V1 delta with setProperty', () => { + const delta = createDelta(creator, host) .setProperty('entity-1', 'name', 'Test Entity') - .build(); + .buildV1(); expect(delta).toBeInstanceOf(DeltaV1); - expect(delta.pointers).toContainEqual({ - localContext: 'name', - target: 'Test Entity', - targetContext: 'name' - }); 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); @@ -56,9 +79,9 @@ describe('DeltaBuilder', () => { }); 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') - .build(); + .buildV1(); expect(delta.pointers).toContainEqual({ localContext: 'follows', @@ -75,8 +98,7 @@ describe('DeltaBuilder', () => { describe('V2 Deltas', () => { it('should create a basic V2 delta', () => { - const builder = new DeltaBuilder(creator, host, 'v2'); - const delta = builder + const delta = createDelta(creator, host) .addPointer('name', 'Test Delta V2', 'title') .buildV2(); @@ -89,7 +111,7 @@ describe('DeltaBuilder', () => { }); 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') .buildV2(); @@ -98,7 +120,7 @@ describe('DeltaBuilder', () => { }); 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') .buildV2(); @@ -112,7 +134,7 @@ describe('DeltaBuilder', () => { const customId = 'custom-delta-id'; const delta = createDelta(creator, host) .withId(customId) - .build(); + .buildV1(); expect(delta.id).toBe(customId); }); @@ -121,7 +143,7 @@ describe('DeltaBuilder', () => { const txId = 'tx-123'; const delta = createDelta(creator, host) .inTransaction(txId) - .build(); + .buildV1(); // Check for transaction ID in pointers const txPointer = delta.pointers.find(p => p.localContext === '_transaction'); @@ -131,7 +153,7 @@ describe('DeltaBuilder', () => { it('should support transactions in V2', () => { const txId = 'tx-123'; - const delta = createDelta(creator, host, 'v2') + const delta = createDelta(creator, host) .inTransaction(txId) .buildV2(); @@ -143,7 +165,7 @@ describe('DeltaBuilder', () => { const negatedId = 'delta-to-negate'; const delta = createDelta(creator, host) .negate(negatedId) - .build(); + .buildV1(); // Check for negation in pointers const negationPointer = delta.pointers.find(p => p.localContext === '_negation'); @@ -155,7 +177,7 @@ describe('DeltaBuilder', () => { const timestamp = Date.now(); const delta = createDelta(creator, host) .withTimestamp(timestamp) - .build(); + .buildV1(); expect(delta.timeCreated).toBe(timestamp); }); diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index 484d915..47d5413 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -1,24 +1,7 @@ -import { - DeltaID, - Delta, - DeltaV1, - DeltaV2, - DeltaNetworkImageV1, - DeltaNetworkImageV2, - PointerTarget, - PointersV2 -} from './delta'; +import { DeltaV1, DeltaV2 } from './delta'; import { randomUUID } from 'crypto'; -import { microtime } from '../utils/time'; - -type DeltaVersion = 'v1' | 'v2'; - -// Local type for V1 pointers -interface PointerV1 { - localContext: string; - target: PointerTarget; - targetContext?: string; -} +import Debug from 'debug'; +const debug = Debug('rz:delta-builder'); /** * A fluent builder for creating Delta objects with proper validation and type safety. @@ -26,12 +9,10 @@ interface PointerV1 { */ export class DeltaBuilder { private id: string; - private timeCreated: number; + private timeCreated?: number; private host: string; private creator: string; - private version: DeltaVersion = 'v2'; // Default to V2 - private pointersV1: Array<{ localContext: string; target: PointerTarget; targetContext?: string }> = []; - private pointersV2: Record = {}; + private pointers: Record = {}; private transactionId?: string; private isNegation: boolean = false; private negatedDeltaId?: string; @@ -40,14 +21,11 @@ export class DeltaBuilder { * 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 - * @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.timeCreated = microtime.now(); this.creator = creator; 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 { - if (this.version === 'v1') { - this.pointersV1.push({ localContext, target, targetContext }); + if (targetContext && typeof target === 'string') { + this.pointers[localContext] = { [target]: targetContext }; } else { - // For V2, we need to handle the target context differently - if (targetContext && typeof target === 'string') { - this.pointersV2[localContext] = { [target]: targetContext }; - } else { - this.pointersV2[localContext] = target; - } + this.pointers[localContext] = target; } 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 { - if (this.version === 'v1') { - // For V1, we need to ensure target is a valid type - 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 }; - } + setProperty(entityId: string, property: string, value: string | number | boolean, entityLabel = "entity"): this { + this.addPointer(entityLabel, entityId, property) + this.addPointer(property, value); return this; } @@ -135,41 +86,17 @@ export class DeltaBuilder { * Create a relationship between two entities */ relate(sourceId: string, relationship: string, targetId: string): this { - if (this.version === 'v1') { - this.pointersV1.push({ - 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 }; - } + this.pointers[relationship] = { [targetId]: relationship }; + this.pointers.source = { [sourceId]: relationship }; 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 */ buildV2(): DeltaV2 { // For V2, we'll store transaction and negation info in the pointers object - const pointers = { ...this.pointersV2 }; + const pointers = { ...this.pointers }; if (this.transactionId) { pointers['_transaction'] = this.transactionId; @@ -182,9 +109,9 @@ export class DeltaBuilder { // Create the delta with all pointers return new DeltaV2({ id: this.id, - timeCreated: this.timeCreated, host: this.host, creator: this.creator, + timeCreated: this.timeCreated, pointers }); } @@ -192,38 +119,14 @@ export class DeltaBuilder { /** * Build and return a DeltaV1 instance */ - private buildV1(): DeltaV1 { - // For V1, we'll store transaction and negation info in the pointers - 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 - }); + buildV1(): DeltaV1 { + return this.buildV2().toV1(); } } /** * Create a new DeltaBuilder instance (convenience function) */ -export function createDelta(creator: string, host: string, version: DeltaVersion = 'v2'): DeltaBuilder { - return new DeltaBuilder(creator, host, version); +export function createDelta(creator: string, host: string): DeltaBuilder { + return new DeltaBuilder(creator, host); } diff --git a/src/utils/time.ts b/src/utils/time.ts deleted file mode 100644 index 3058d8b..0000000 --- a/src/utils/time.ts +++ /dev/null @@ -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 -}; From 4542c4ce836774860d231c8a7069ad970c940304 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 10:46:34 -0500 Subject: [PATCH 4/7] Use deltaBuilder in lossless.decompose --- .windsurf/workflows/delta-builder.md | 6 ++++ .windsurf/workflows/delta-format.md | 7 ---- src/views/lossless.ts | 53 +++++++++++++++------------- 3 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 .windsurf/workflows/delta-builder.md delete mode 100644 .windsurf/workflows/delta-format.md diff --git a/.windsurf/workflows/delta-builder.md b/.windsurf/workflows/delta-builder.md new file mode 100644 index 0000000..64cc47c --- /dev/null +++ b/.windsurf/workflows/delta-builder.md @@ -0,0 +1,6 @@ +--- +description: Update the current file to use delta builder +--- + +Replace each deltav2 instantiation with a fluent call to createDelta from delta builder, using the following process: + - pass creator and host as arguments to createDelta \ No newline at end of file diff --git a/.windsurf/workflows/delta-format.md b/.windsurf/workflows/delta-format.md deleted file mode 100644 index 5f0bb63..0000000 --- a/.windsurf/workflows/delta-format.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Update deltas to use the object style for pointers ---- - -- in the current file, for each v1 delta, rewrite it as a v2 delta - - make sure the new delta is isomorphic to the original - - do not include a timestamp \ No newline at end of file diff --git a/src/views/lossless.ts b/src/views/lossless.ts index a36fee0..a1ed380 100644 --- a/src/views/lossless.ts +++ b/src/views/lossless.ts @@ -9,6 +9,7 @@ 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}; @@ -199,31 +200,35 @@ export class Lossless { for (const delta of deltas) { if (!seenDeltaIds.has(delta.id)) { seenDeltaIds.add(delta.id); - // 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 }; - } + + // 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 } - // Fallback for unexpected pointer structure - return { localContext: 'unknown', target: 'unknown' }; - }) - }); - allDeltas.push(fullDelta); + 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()); } } } From 5c1c8a23b8baaa04dd1d4c33bd6af1e847931736 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 11:04:55 -0500 Subject: [PATCH 5/7] refactor: update delta declarations to use DeltaBuilder in lossless tests - Refactored delta declarations in __tests__/lossless.ts to use DeltaBuilder - Added declareTransaction method to DeltaBuilder for better transaction handling - Updated transaction pointer structure in DeltaBuilder to match expected format - Fixed related tests to work with the new DeltaBuilder implementation --- __tests__/delta-builder.test.ts | 2 +- __tests__/lossless.ts | 128 +++++++++++++------------------- src/core/delta-builder.ts | 14 +++- 3 files changed, 67 insertions(+), 77 deletions(-) diff --git a/__tests__/delta-builder.test.ts b/__tests__/delta-builder.test.ts index d120ee3..95ad931 100644 --- a/__tests__/delta-builder.test.ts +++ b/__tests__/delta-builder.test.ts @@ -158,7 +158,7 @@ describe('DeltaBuilder', () => { .buildV2(); // Check for transaction ID in V2 pointers - expect(delta.pointers['_transaction']).toBe(txId); + expect(delta.pointers['_transaction']).toEqual({ [txId]: 'deltas' }); }); it('should support negation', () => { diff --git a/__tests__/lossless.ts b/__tests__/lossless.ts index af4a08b..65316db 100644 --- a/__tests__/lossless.ts +++ b/__tests__/lossless.ts @@ -1,22 +1,19 @@ 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 = new DeltaV2({ - creator: 'a', - host: 'h', - pointers: { - actor: {"keanu": "roles"}, - role: {"neo": "actor"}, - film: {"the_matrix": "cast"}, - base_salary: 1000000, - salary_currency: "usd" - } - }).toV1(); + 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(); expect(delta.pointers).toMatchObject([{ localContext: "actor", @@ -95,17 +92,13 @@ describe('Lossless', () => { }); it('accepts DeltaV2 instances', () => { - 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" - } - }); + 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); @@ -167,26 +160,20 @@ describe('Lossless', () => { const lossless = new Lossless(node); beforeAll(() => { - lossless.ingestDelta(new Delta({ - creator: 'A', - host: 'H', - pointers: [{ - localContext: "1", - target: "ace", - targetContext: "value" - }] - })); + // First delta + lossless.ingestDelta( + createDelta('A', 'H') + .addPointer('1', 'ace', 'value') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'B', - host: 'H', - pointers: [{ + // Second delta + lossless.ingestDelta( + createDelta('B', 'H') // 10 11j 12q 13k 14a - localContext: "14", - target: "ace", - targetContext: "value" - }] - })); + .addPointer('14', 'ace', 'value') + .buildV1() + ); expect(lossless.view()).toMatchObject({ ace: { @@ -251,51 +238,42 @@ describe('Lossless', () => { const transactionId = 'tx-filter-test'; // Declare transaction with 3 deltas - losslessT.ingestDelta(new Delta({ - creator: 'system', - host: 'H', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 3 } - ] - })); + losslessT.ingestDelta( + createDelta('system', 'H') + .declareTransaction(transactionId, 3) + .buildV1() + ); // A1: First delta from creator A - 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' } - ] - })); + losslessT.ingestDelta( + createDelta('A', 'H') + .inTransaction(transactionId) + .addPointer('step', 'process1', 'status') + .addPointer('value', 'started') + .buildV1() + ); // B: Delta from creator B - 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' } - ] - })); + losslessT.ingestDelta( + createDelta('B', 'H') + .inTransaction(transactionId) + .addPointer('step', 'process1', 'status') + .addPointer('value', 'processing') + .buildV1() + ); // Transaction incomplete - nothing should show const incompleteView = losslessT.view(['process1']); expect(incompleteView.process1).toBeUndefined(); // A2: Second delta from creator A completes transaction - 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' } - ] - })); + losslessT.ingestDelta( + createDelta('A', 'H') + .inTransaction(transactionId) + .addPointer('step', 'process1', 'status') + .addPointer('value', 'completed') + .buildV1() + ); // All deltas visible now const completeView = losslessT.view(['process1']); diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index 47d5413..f08f493 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -51,6 +51,18 @@ export class DeltaBuilder { this.transactionId = transactionId; 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.addPointer('_transaction', transactionId, 'size'); + this.addPointer('size', size) + return this; + } /** * Mark this delta as a negation of another delta @@ -99,7 +111,7 @@ export class DeltaBuilder { const pointers = { ...this.pointers }; if (this.transactionId) { - pointers['_transaction'] = this.transactionId; + pointers['_transaction'] = { [this.transactionId]: 'deltas' }; } if (this.isNegation && this.negatedDeltaId) { From 60ad920b305ec1f2613f0be681886e6fa3de9ca7 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 21:40:51 -0500 Subject: [PATCH 6/7] refactor: update test files to use DeltaBuilder fluent API - Refactored delta creation in test files to use createDelta() pattern - Replaced direct Delta instantiations with fluent builder API - Updated relationship deltas to use setProperty with proper entity context - Ensured all tests pass with the new delta creation approach This is part of the ongoing effort to standardize on the DeltaBuilder API across the codebase for better consistency and maintainability. --- .windsurf/workflows/delta-builder.md | 5 +- __tests__/aggregation-resolvers.ts | 485 ++++--------- __tests__/compose-decompose.ts | 128 ++-- __tests__/concurrent-writes.ts | 481 ++++--------- __tests__/custom-resolvers.ts | 653 ++++++------------ __tests__/delta.ts | 29 +- __tests__/jest-setup.ts | 8 +- __tests__/last-write-wins.ts | 33 +- __tests__/lossy.ts | 33 +- __tests__/multi-pointer-resolution.ts | 56 +- __tests__/negation.ts | 293 +++----- __tests__/nested-resolution-performance.ts | 35 +- __tests__/nested-resolution.ts | 122 ++-- __tests__/query.ts | 107 +-- __tests__/run/001-single-node-orchestrated.ts | 10 - __tests__/run/002-two-nodes-orchestrated.ts | 15 - __tests__/run/005-docker-orchestrator.ts | 108 +-- __tests__/schema.ts | 28 +- __tests__/storage.ts | 46 +- __tests__/timestamp-resolvers.ts | 439 ++++-------- __tests__/transactions.ts | 410 +++++------ package.json | 9 +- src/core/delta-builder.ts | 7 + src/core/index.ts | 1 + .../managers/image-manager.ts | 20 +- src/query/query-engine.ts | 18 +- src/query/storage-query-engine.ts | 12 +- 27 files changed, 1165 insertions(+), 2426 deletions(-) diff --git a/.windsurf/workflows/delta-builder.md b/.windsurf/workflows/delta-builder.md index 64cc47c..deca0b2 100644 --- a/.windsurf/workflows/delta-builder.md +++ b/.windsurf/workflows/delta-builder.md @@ -2,5 +2,6 @@ description: Update the current file to use delta builder --- -Replace each deltav2 instantiation with a fluent call to createDelta from delta builder, using the following process: - - pass creator and host as arguments to createDelta \ No newline at end of file +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 \ No newline at end of file diff --git a/__tests__/aggregation-resolvers.ts b/__tests__/aggregation-resolvers.ts index 2038de2..46bff08 100644 --- a/__tests__/aggregation-resolvers.ts +++ b/__tests__/aggregation-resolvers.ts @@ -1,7 +1,6 @@ import { RhizomeNode, Lossless, - Delta, AggregationResolver, MinResolver, MaxResolver, @@ -10,6 +9,8 @@ import { CountResolver, AggregationType } from "../src"; +import { createDelta } from "../src/core/delta-builder"; +import { Delta } from "../src/core/delta"; describe('Aggregation Resolvers', () => { let node: RhizomeNode; @@ -23,46 +24,22 @@ describe('Aggregation Resolvers', () => { describe('Basic Aggregation', () => { test('should aggregate numbers using min resolver', () => { // Add first entity with score 10 - 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', 10, 'collection') + .buildV1() + ); // Add second entity with score 5 - 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('entity2', 'score', 5, 'collection') + .buildV1() + ); // Add third entity with score 15 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity3", - targetContext: "score" - }, { - localContext: "score", - target: 15 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity3', 'score', 15, 'collection') + .buildV1() + ); const minResolver = new MinResolver(lossless, ['score']); const result = minResolver.resolve(); @@ -76,44 +53,20 @@ describe('Aggregation Resolvers', () => { test('should aggregate numbers using max resolver', () => { // Add deltas for entities - 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', 10, '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('entity2', 'score', 5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity3", - targetContext: "score" - }, { - localContext: "score", - target: 15 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity3', 'score', 15, 'collection') + .buildV1() + ); const maxResolver = new MaxResolver(lossless, ['score']); const result = maxResolver.resolve(); @@ -126,46 +79,22 @@ describe('Aggregation Resolvers', () => { test('should aggregate numbers using sum resolver', () => { // Add first value for entity1 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); // Add second value for entity1 (should sum) - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 20, 'collection') + .buildV1() + ); // Add value for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "value" - }, { - localContext: "value", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'value', 5, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['value']); const result = sumResolver.resolve(); @@ -176,46 +105,22 @@ describe('Aggregation Resolvers', () => { }); test('should aggregate numbers using average resolver', () => { - // Add multiple values for entity1 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + // Add multiple scores for entity1 + 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: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 20, 'collection') + .buildV1() + ); // Single value for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "score" - }, { - localContext: "score", - target: 30 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'score', 30, 'collection') + .buildV1() + ); const avgResolver = new AverageResolver(lossless, ['score']); const result = avgResolver.resolve(); @@ -227,45 +132,21 @@ describe('Aggregation Resolvers', () => { test('should count values using count resolver', () => { // Add multiple visit deltas for entity1 - 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 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'visits', 1, 'collection') + .buildV1() + ); // Single visit for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "visits" - }, { - localContext: "visits", - target: 1 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'visits', 1, 'collection') + .buildV1() + ); const countResolver = new CountResolver(lossless, ['visits']); const result = countResolver.resolve(); @@ -279,84 +160,36 @@ describe('Aggregation Resolvers', () => { describe('Custom Aggregation Configuration', () => { test('should handle mixed aggregation types', () => { // Add first set of values - 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', 'min_val', 10, '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', 'max_val', 5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "sum_val" - }, { - localContext: "sum_val", - target: 3 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'sum_val', 3, 'collection') + .buildV1() + ); // Add second set of values - 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', 'min_val', 5, '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', 'max_val', 15, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "sum_val" - }, { - localContext: "sum_val", - target: 7 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'sum_val', 7, 'collection') + .buildV1() + ); const resolver = new AggregationResolver(lossless, { min_val: 'min' as AggregationType, @@ -375,46 +208,22 @@ describe('Aggregation Resolvers', () => { test('should ignore non-numeric values', () => { // Add numeric value - 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', 10, 'collection') + .buildV1() + ); // Add non-numeric value (string) - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); // Add another numeric value - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 20, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['score', 'name']); const result = sumResolver.resolve(); @@ -427,18 +236,10 @@ describe('Aggregation Resolvers', () => { test('should handle empty value arrays', () => { // Create entity with non-aggregated property - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['score']); const result = sumResolver.resolve(); @@ -451,18 +252,10 @@ describe('Aggregation Resolvers', () => { describe('Edge Cases', () => { test('should handle single value aggregations', () => { - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 42 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 42, 'collection') + .buildV1() + ); const avgResolver = new AverageResolver(lossless, ['value']); const result = avgResolver.resolve(); @@ -472,31 +265,15 @@ describe('Aggregation Resolvers', () => { }); test('should handle zero values', () => { - 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', 0, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['value']); const result = sumResolver.resolve(); @@ -506,31 +283,15 @@ describe('Aggregation Resolvers', () => { }); test('should handle negative values', () => { - 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', -5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); const minResolver = new MinResolver(lossless, ['value']); const result = minResolver.resolve(); diff --git a/__tests__/compose-decompose.ts b/__tests__/compose-decompose.ts index d3b64fd..160b630 100644 --- a/__tests__/compose-decompose.ts +++ b/__tests__/compose-decompose.ts @@ -5,7 +5,7 @@ */ import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; describe('Lossless View Compose/Decompose', () => { let node: RhizomeNode; @@ -18,22 +18,14 @@ describe('Lossless View Compose/Decompose', () => { it('should compose and decompose simple entity deltas correctly', () => { // Create simple entity deltas const nameDeltas = [ - 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' } - ] - }) + 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() ]; // Ingest the deltas @@ -73,17 +65,13 @@ describe('Lossless View Compose/Decompose', () => { it('should handle multi-pointer relationship deltas correctly', () => { // Create a complex relationship delta - 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 } - ] - }); + 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(); node.lossless.ingestDelta(relationshipDelta); @@ -115,33 +103,21 @@ describe('Lossless View Compose/Decompose', () => { it('should handle reference relationships correctly', () => { // Create entities first - const aliceDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice' } - ] - }); + const aliceDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice') + .buildV1(); - const bobDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'bob', targetContext: 'name' }, - { localContext: 'name', target: 'Bob' } - ] - }); + const bobDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'bob', 'name') + .addPointer('name', 'Bob') + .buildV1(); // Create friendship relationship - const friendshipDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friend', target: 'bob', targetContext: 'friends' } - ] - }); + const friendshipDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'friends') + .addPointer('friend', 'bob', 'friends') + .buildV1(); [aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d)); @@ -171,14 +147,10 @@ describe('Lossless View Compose/Decompose', () => { }); it('should preserve delta metadata correctly', () => { - const originalDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice' } - ] - }); + const originalDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice') + .buildV1(); node.lossless.ingestDelta(originalDelta); @@ -198,30 +170,18 @@ describe('Lossless View Compose/Decompose', () => { it('should handle multiple deltas for the same property', () => { // Create multiple name changes for alice const nameDeltas = [ - 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' } - ] - }) + 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() ]; nameDeltas.forEach(d => node.lossless.ingestDelta(d)); diff --git a/__tests__/concurrent-writes.ts b/__tests__/concurrent-writes.ts index f16bb5b..97c0c55 100644 --- a/__tests__/concurrent-writes.ts +++ b/__tests__/concurrent-writes.ts @@ -1,3 +1,4 @@ +import { createDelta } from '../src/core/delta-builder'; import { RhizomeNode, Lossless, @@ -24,35 +25,19 @@ describe('Concurrent Write Scenarios', () => { const timestamp = 1000; // Simulate two writers updating the same property at the exact same time - 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('writer1', 'host1') + .withId('delta-a') + .withTimestamp(timestamp) + .setProperty('entity1', 'score', 100, '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 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withId('delta-b') + .withTimestamp(timestamp) // Same timestamp + .setProperty('entity1', 'score', 200, 'collection') + .buildV1() + ); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); @@ -66,35 +51,19 @@ describe('Concurrent Write Scenarios', () => { test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => { const timestamp = 1000; - 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_z', 'host1') // Lexicographically later + .withId('delta-a') + .withTimestamp(timestamp) + .setProperty('entity1', 'score', 100, '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 - }] - })); + lossless.ingestDelta(createDelta('writer_a', 'host2') // Lexicographically earlier + .withId('delta-b') + .withTimestamp(timestamp) // Same timestamp + .setProperty('entity1', 'score', 200, 'collection') + .buildV1() + ); const resolver = new TimestampResolver(lossless, 'creator-id'); const result = resolver.resolve(); @@ -108,47 +77,24 @@ describe('Concurrent Write Scenarios', () => { const timestamp = 1000; // Multiple writers add values simultaneously - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "points" - }, { - localContext: "points", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'points', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer2', - host: 'host2', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "points" - }, { - localContext: "points", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withTimestamp(1000) // Same timestamp + .setProperty('entity1', 'points', 20, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer3', - host: 'host3', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "points" - }, { - localContext: "points", - target: 30 - }] - })); + // Third writer adds another value + lossless.ingestDelta(createDelta('writer3', 'host3') + .withTimestamp(1000) // Same timestamp + .setProperty('entity1', 'points', 30, 'collection') + .buildV1() + ); const resolver = new SumResolver(lossless, ['points']); const result = resolver.resolve(); @@ -162,34 +108,20 @@ 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(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: 2000, // Later timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 'newer' - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 'newer') + .buildV1() + ); // Older delta arrives later - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: 1000, // Earlier timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 'older' - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 'older') + .buildV1() + ); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); @@ -201,47 +133,26 @@ describe('Concurrent Write Scenarios', () => { test('should maintain correct aggregation despite out-of-order arrival', () => { // Add deltas in reverse chronological order - 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(3000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 30) + .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(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new SumResolver(lossless, ['score']); const result = resolver.resolve(); @@ -261,19 +172,12 @@ 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(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 - }] - })); + lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`) + .withTimestamp(baseTimestamp + write) + .addPointer('collection', 'entity1', 'counter') + .addPointer('counter', 1) + .buildV1() + ); } } @@ -289,62 +193,34 @@ describe('Concurrent Write Scenarios', () => { const timestamp = 1000; // Writer 1 updates name and score - 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) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: timestamp + 1, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(timestamp + 1) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 100) + .buildV1() + ); // Writer 2 updates name and score concurrently - 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 + 2) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'bob') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer2', - host: 'host2', - timeCreated: timestamp + 3, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 200 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withTimestamp(timestamp + 3) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 200) + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin(), @@ -365,19 +241,12 @@ describe('Concurrent Write Scenarios', () => { // Multiple writers updating different entities simultaneously for (let i = 0; i < 5; i++) { - 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 - }] - })); + lossless.ingestDelta(createDelta(`writer${i}`, `host${i}`) + .withTimestamp(timestamp) + .addPointer('collection', `entity${i}`, 'value') + .addPointer('value', (i + 1) * 10) + .buildV1() + ); } const resolver = new LastWriteWins(lossless); @@ -395,62 +264,34 @@ describe('Concurrent Write Scenarios', () => { const timestamp = 1000; // Entity1: Multiple writers competing for same property - 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('writer1', 'host1') + .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('writer2', 'host2') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'votes') + .addPointer('votes', 'option_a') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer3', - host: 'host3', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "votes" - }, { - localContext: "votes", - target: 'option_b' - }] - })); + lossless.ingestDelta(createDelta('writer3', 'host3') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'votes') + .addPointer('votes', 'option_b') + .buildV1() + ); // Entity2: Single writer, no conflict - lossless.ingestDelta(new Delta({ - creator: 'writer4', - host: 'host4', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "status" - }, { - localContext: "status", - target: 'active' - }] - })); + lossless.ingestDelta(createDelta('writer4', 'host4') + .withTimestamp(timestamp) + .addPointer('collection', 'entity2', 'status') + .addPointer('status', 'active') + .buildV1() + ); const resolver = new CustomResolver(lossless, { votes: new MajorityVotePlugin(), @@ -474,19 +315,12 @@ 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(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 - }] - })); + 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() + ); } } @@ -510,19 +344,15 @@ describe('Concurrent Write Scenarios', () => { // Add initial deltas for (let i = 0; i < 50; i++) { - 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 - }] - })); + lossless.ingestDelta(createDelta( + `writer${i % 5}`, + `host${i % 3}` + ) + .withTimestamp(1000 + i) + .addPointer('collection', entityId, 'counter') + .addPointer('counter', 1) + .buildV1() + ); updateCount++; } @@ -534,19 +364,12 @@ describe('Concurrent Write Scenarios', () => { // Add more deltas and verify consistency for (let i = 0; i < 25; i++) { - lossless.ingestDelta(new Delta({ - creator: 'late-writer', - host: 'late-host', - timeCreated: 2000 + i, - pointers: [{ - localContext: "collection", - target: entityId, - targetContext: "counter" - }, { - localContext: "counter", - target: 2 - }] - })); + lossless.ingestDelta(createDelta('late-writer', 'late-host') + .withTimestamp(2000 + i) + .addPointer('collection', entityId, 'counter') + .addPointer('counter', 2) + .buildV1() + ); updateCount += 2; // Create a fresh resolver to avoid accumulator caching issues diff --git a/__tests__/custom-resolvers.ts b/__tests__/custom-resolvers.ts index b6d3a70..6c3b7c1 100644 --- a/__tests__/custom-resolvers.ts +++ b/__tests__/custom-resolvers.ts @@ -11,7 +11,8 @@ import { MinPlugin, MaxPlugin, PropertyTypes, - CollapsedDelta + CollapsedDelta, + createDelta } from "../src"; describe('Custom Resolvers', () => { @@ -25,33 +26,21 @@ describe('Custom Resolvers', () => { describe('Built-in Plugins', () => { test('LastWriteWinsPlugin should resolve to most recent value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'first' - }] - })); + // 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: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'second' - }] - })); + // Second delta with later timestamp (should win) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'name', 'second', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin() @@ -63,33 +52,21 @@ describe('Custom Resolvers', () => { }); test('FirstWriteWinsPlugin should resolve to earliest value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'second' - }] - })); + // 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: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'first' - }] - })); + // Earlier delta (should win with FirstWriteWins) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'first', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new FirstWriteWinsPlugin() @@ -101,47 +78,29 @@ describe('Custom Resolvers', () => { }); test('ConcatenationPlugin should join string values chronologically', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' - }] - })); + // First tag + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'tags', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'blue' - }] - })); + // 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: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'green' - }] - })); + // Third tag (with timestamp between first and second) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'tags', 'green', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { tags: new ConcatenationPlugin(' ') @@ -153,33 +112,21 @@ describe('Custom Resolvers', () => { }); test('ConcatenationPlugin should handle duplicates', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' - }] - })); + // First tag + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'tags', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' // duplicate - }] - })); + // Duplicate tag with later timestamp + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'tags', 'red', 'collection') // duplicate + .buildV1() + ); const resolver = new CustomResolver(lossless, { tags: new ConcatenationPlugin(',') @@ -192,76 +139,41 @@ describe('Custom Resolvers', () => { test('MajorityVotePlugin should resolve to most voted value', () => { // Add 3 votes for 'red' - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'red' - }] - })); + lossless.ingestDelta( + createDelta('user1', '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('user2', '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' - }] - })); + lossless.ingestDelta( + createDelta('user3', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'red', 'collection') + .buildV1() + ); // Add 2 votes for 'blue' - lossless.ingestDelta(new Delta({ - creator: 'user4', - host: 'host1', - timeCreated: 1003, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'blue' - }] - })); + lossless.ingestDelta( + createDelta('user4', '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' - }] - })); + lossless.ingestDelta( + createDelta('user5', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'blue', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { color: new MajorityVotePlugin() @@ -273,47 +185,29 @@ describe('Custom Resolvers', () => { }); test('MinPlugin should resolve to minimum numeric value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + // 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: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 50 - }] - })); + // 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: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 75 - }] - })); + // Third score (75) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 75, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MinPlugin() @@ -325,47 +219,29 @@ describe('Custom Resolvers', () => { }); test('MaxPlugin should resolve to maximum numeric value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + // 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: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 150 - }] - })); + // 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: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 75 - }] - })); + // Third score (75) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 75, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MaxPlugin() @@ -380,62 +256,36 @@ describe('Custom Resolvers', () => { describe('Mixed Plugin Configurations', () => { test('should handle different plugins for different properties', () => { // Add name with different timestamps - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'old_name' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'old_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' - }] - })); + // Update name with newer timestamp + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'name', 'new_name', 'collection') + .buildV1() + ); // Add scores - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 50 - }] - })); + // Add another score (MinPlugin will pick the smaller one) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'score', 50, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin(), // Should resolve to 'new_name' @@ -450,34 +300,20 @@ describe('Custom Resolvers', () => { test('should only include entities with configured properties', () => { // Entity1 has configured property - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); // Entity2 has non-configured property - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "other" - }, { - localContext: "other", - target: 'value' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity2', 'other_prop', 'value', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin() @@ -510,47 +346,29 @@ describe('Custom Resolvers', () => { } } - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'first' - }] - })); + // First update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'updates', 'first', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'second' - }] - })); + // Second update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'updates', 'second', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'third' - }] - })); + // Third update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'updates', 'third', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { updates: new CountPlugin() @@ -585,47 +403,29 @@ describe('Custom Resolvers', () => { } } - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + // 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: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + // 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: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 30 - }] - })); + // Third score (30) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 30, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new RunningAveragePlugin() @@ -650,19 +450,12 @@ describe('Custom Resolvers', () => { test('should handle non-matching property types gracefully', () => { // Add string value to numeric plugin - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 'not_a_number' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 'not_a_number', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MinPlugin() // Expects numeric values diff --git a/__tests__/delta.ts b/__tests__/delta.ts index 2af876f..95da3cf 100644 --- a/__tests__/delta.ts +++ b/__tests__/delta.ts @@ -1,19 +1,12 @@ +import { createDelta } from '../src/core/delta-builder'; import {DeltaV1, DeltaV2} from "../src"; describe("Delta", () => { it("can convert DeltaV1 to DeltaV2", () => { - const deltaV1 = new DeltaV1({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: 'color', - target: 'red' - }, { - localContext: 'furniture', - target: 'chair-1', - targetContext: 'color' - }] - }); + const deltaV1 = createDelta('a', 'h') + .addPointer('color', 'red') + .addPointer('furniture', 'chair-1', 'color') + .buildV1(); const deltaV2 = DeltaV2.fromV1(deltaV1); @@ -27,14 +20,10 @@ describe("Delta", () => { }); it("can convert DeltaV2 to DeltaV1", () => { - const deltaV2 = new DeltaV2({ - creator: 'a', - host: 'h', - pointers: { - color: 'red', - furniture: {'chair-1': 'color'} - } - }); + const deltaV2 = createDelta('a', 'h') + .addPointer('color', 'red') + .addPointer('furniture', 'chair-1', 'color') + .buildV2(); const deltaV1 = deltaV2.toV1(); diff --git a/__tests__/jest-setup.ts b/__tests__/jest-setup.ts index 0f33cc9..df58a9c 100644 --- a/__tests__/jest-setup.ts +++ b/__tests__/jest-setup.ts @@ -1,5 +1,11 @@ // Set up environment variables for tests -process.env.DEBUG = 'rz:*'; +// DEBUG handling examples: +// npm test // will set DEBUG=rz:* by default +// NO_DEBUG=true npm test // will not set DEBUG +// DEBUG=other npm test // will set DEBUG=other +if (!process.env.DEBUG && !process.env.NO_DEBUG) { + process.env.DEBUG = 'rz:*'; +} // Extend the global Jest namespace declare global { diff --git a/__tests__/last-write-wins.ts b/__tests__/last-write-wins.ts index 7303fdf..38a4ddd 100644 --- a/__tests__/last-write-wins.ts +++ b/__tests__/last-write-wins.ts @@ -1,4 +1,5 @@ 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'); @@ -11,31 +12,15 @@ describe('Last write wins', () => { const lossy = new LastWriteWins(lossless); beforeAll(() => { - 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', 95, 'vegetable') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: "vegetable", - target: "broccoli", - targetContext: "want" - }, { - localContext: "want", - target: 90, - }] - })); + lossless.ingestDelta(createDelta('a', 'h') + .setProperty('broccoli', 'want', 90, 'vegetable') + .buildV1() + ); }); it('our resolver should return the most recently written value', () => { diff --git a/__tests__/lossy.ts b/__tests__/lossy.ts index e730389..5c2ad8c 100644 --- a/__tests__/lossy.ts +++ b/__tests__/lossy.ts @@ -1,6 +1,5 @@ import Debug from 'debug'; import { - Delta, PointerTarget, lastValueFromDeltas, valueFromCollapsedDelta, @@ -9,6 +8,7 @@ import { Lossy, RhizomeNode } from "../src"; +import { createDelta } from "../src/core/delta-builder"; const debug = Debug('test:lossy'); type Role = { @@ -62,29 +62,14 @@ describe('Lossy', () => { const lossy = new Summarizer(lossless); beforeAll(() => { - 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" - }] - })); + 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() + ); }); it('example summary', () => { diff --git a/__tests__/multi-pointer-resolution.ts b/__tests__/multi-pointer-resolution.ts index 8b45ba7..7934c47 100644 --- a/__tests__/multi-pointer-resolution.ts +++ b/__tests__/multi-pointer-resolution.ts @@ -5,7 +5,7 @@ */ import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema'; import { TypedCollectionImpl } from '../src/collections'; @@ -76,17 +76,13 @@ 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 = 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' } - ] - }); + 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(); node.lossless.ingestDelta(castingDelta); // Test from Keanu's perspective @@ -164,17 +160,13 @@ describe('Multi-Pointer Delta Resolution', () => { await personCollection.put('bob', { name: 'Bob' }); // Create a relationship delta with one entity reference and multiple scalars - 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 - ] - }); + 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(); node.lossless.ingestDelta(relationshipDelta); // Test from Alice's perspective @@ -243,17 +235,13 @@ describe('Multi-Pointer Delta Resolution', () => { await designerCollection.put('bob', { name: 'Bob Designer' }); // Create a collaboration delta with multiple entity references - 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 - ] - }); + 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(); node.lossless.ingestDelta(collaborationDelta); // Test from project's perspective diff --git a/__tests__/negation.ts b/__tests__/negation.ts index 857f97a..28ee328 100644 --- a/__tests__/negation.ts +++ b/__tests__/negation.ts @@ -1,4 +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'; @@ -17,14 +18,9 @@ describe('Negation System', () => { describe('Negation Helper', () => { it('should create negation deltas correctly', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'entity1', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Alice') + .buildV1(); const negationDelta = NegationHelper.createNegation( originalDelta.id, @@ -44,11 +40,9 @@ describe('Negation System', () => { }); it('should identify negation deltas', () => { - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const regularDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); const negationDelta = NegationHelper.createNegation( 'delta-to-negate', @@ -71,27 +65,21 @@ describe('Negation System', () => { const extractedId = NegationHelper.getNegatedDeltaId(negationDelta); expect(extractedId).toBe(targetDeltaId); - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const regularDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull(); }); it('should find negations for specific deltas', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1'); @@ -110,17 +98,13 @@ describe('Negation System', () => { }); it('should check if deltas are negated', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const allDeltas = [delta1, delta2, negation1]; @@ -130,23 +114,17 @@ describe('Negation System', () => { }); it('should filter out negated deltas', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); - const delta3 = new Delta({ - creator: 'user3', - host: 'host1', - pointers: [{ localContext: 'email', target: 'entity1', targetContext: 'email' }] - }); + const delta3 = createDelta('user3', 'host1') + .setProperty('entity1', 'email', 'entity1@example.com') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1'); @@ -160,17 +138,13 @@ describe('Negation System', () => { }); it('should provide negation statistics', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const allDeltas = [delta1, delta2, negation1]; @@ -189,22 +163,18 @@ describe('Negation System', () => { const baseTime = Date.now(); // Create deltas with specific timestamps - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime, - pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }] - }); + const delta1 = createDelta('user1', 'host1') + .withTimestamp(baseTime) + .setProperty('entity1', 'status', 'active') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); negation1.timeCreated = baseTime + 1000; // 1 second later - const delta2 = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime + 2000, // 2 seconds later - pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }] - }); + const delta2 = createDelta('user1', 'host1') + .withTimestamp(baseTime + 2000) + .setProperty('entity1', 'status', 'inactive') + .buildV1(); const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1'); negation2.timeCreated = baseTime + 3000; // 3 seconds later @@ -220,14 +190,9 @@ describe('Negation System', () => { describe('Lossless View Integration', () => { it('should filter negated deltas in lossless views', () => { // Create original delta - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'user123', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('user123', 'name', 'Alice') + .buildV1(); // Create negation delta const negationDelta = NegationHelper.createNegation( @@ -238,14 +203,9 @@ describe('Negation System', () => { // Create another non-negated delta - const nonNegatedDelta = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'age', target: 'user123', targetContext: 'age' }, - { localContext: 'value', target: 25 } - ] - }); + const nonNegatedDelta = createDelta('user2', 'host1') + .setProperty('user123', 'age', 25) + .buildV1(); // Ingest all deltas lossless.ingestDelta(originalDelta); @@ -263,14 +223,9 @@ describe('Negation System', () => { }); it('should handle multiple negations of the same delta', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Original content' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('post1', 'content', 'Original content') + .buildV1(); const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1'); @@ -286,23 +241,13 @@ describe('Negation System', () => { }); it('should provide negation statistics for entities', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'title', target: 'article1', targetContext: 'title' }, - { localContext: 'value', target: 'Original Title' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('article1', 'title', 'Original Title') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'article1', targetContext: 'content' }, - { localContext: 'value', target: 'Article content' } - ] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('article1', 'content', 'Article content') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); @@ -321,14 +266,9 @@ describe('Negation System', () => { }); it('should retrieve negation deltas for entities', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'status', target: 'task1', targetContext: 'status' }, - { localContext: 'value', target: 'pending' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('task1', 'status', 'pending') + .buildV1(); const negationDelta = NegationHelper.createNegation( originalDelta.id, @@ -349,25 +289,16 @@ describe('Negation System', () => { const transactionId = 'tx-negation'; // Create transaction declaration - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(transactionId, 2) + .buildV1() + ); // Create original delta in transaction - 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' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .declareTransaction(transactionId, 2) + .setProperty('post1', 'comments', 'Inappropriate comment') + .buildV1(); // Create negation delta in same transaction const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1'); @@ -389,30 +320,20 @@ describe('Negation System', () => { const baseTime = Date.now(); // User posts content - const postDelta = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime, - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Original post' } - ] - }); + const postDelta = createDelta('user1', 'host1') + .withTimestamp(baseTime) + .setProperty('post1', 'content', 'Original post') + .buildV1(); // Moderator negates it const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1'); negationDelta.timeCreated = baseTime + 1000; // User edits content (after negation) - const editDelta = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime + 2000, - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Edited post' } - ] - }); + const editDelta = createDelta('user1', 'host1') + .withTimestamp(baseTime + 2000) + .setProperty('post1', 'content', 'Edited post') + .buildV1(); lossless.ingestDelta(postDelta); lossless.ingestDelta(negationDelta); @@ -447,14 +368,10 @@ describe('Negation System', () => { it('should handle self-referential entities in negations', () => { // Create a delta that references itself - const selfRefDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'parent', target: 'node1', targetContext: 'parent' }, - { localContext: 'child', target: 'node1' } // Self-reference - ] - }); + const selfRefDelta = createDelta('user1', 'host1') + .setProperty('node1', 'parent', 'node1') + .setProperty('node1', 'child', 'node1') // Self-reference + .buildV1(); const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1'); @@ -470,14 +387,9 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create the original delta - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'title', target: 'entity2', targetContext: 'title' }, - { localContext: 'status', target: 'Draft' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('entity2', 'title', 'Draft') + .buildV1(); // Create two negations of the same delta const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1'); @@ -506,14 +418,9 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create the original delta - const deltaA = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'entity3', targetContext: 'content' }, - { localContext: 'text', target: 'Hello World' } - ] - }); + const deltaA = createDelta('user1', 'host1') + .setProperty('entity3', 'content', 'Hello World') + .buildV1(); // Create a chain of negations: B negates A, C negates B, D negates C const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1'); @@ -584,23 +491,13 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create two independent deltas - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'item', target: 'entity4', targetContext: 'item' }, - { localContext: 'name', target: 'Item 1' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity4', 'item', 'Item 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'item', target: 'entity4', targetContext: 'item' }, - { localContext: 'name', target: 'Item 2' } - ] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity4', 'item', 'Item 2') + .buildV1(); // Create negations for both deltas const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1'); diff --git a/__tests__/nested-resolution-performance.ts b/__tests__/nested-resolution-performance.ts index 35b7e41..e3904c2 100644 --- a/__tests__/nested-resolution-performance.ts +++ b/__tests__/nested-resolution-performance.ts @@ -12,7 +12,7 @@ import Debug from 'debug'; import { RhizomeNode } from '../src/node'; const debug = Debug('rz:test:nested-resolution-performance'); -import { Delta } from '../src/core'; +import { Delta, createDelta } from '../src/core'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema'; import { TypedCollectionImpl } from '../src/collections'; @@ -80,14 +80,9 @@ describe('Nested Object Resolution Performance', () => { const friendIndex = Math.floor(Math.random() * userCount); if (friendIndex !== i) { const friendId = userIds[friendIndex]; - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'friends' }, - { localContext: 'friends', target: friendId } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'friends', friendId, 'users') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); } } @@ -98,14 +93,9 @@ describe('Nested Object Resolution Performance', () => { const followerIndex = Math.floor(Math.random() * userCount); if (followerIndex !== i) { const followerId = userIds[followerIndex]; - const followDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'followers' }, - { localContext: 'followers', target: followerId } - ] - }); + const followDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'followers', followerId, 'users') + .buildV1(); node.lossless.ingestDelta(followDelta); } } @@ -114,14 +104,9 @@ 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 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'mentor' }, - { localContext: 'mentor', target: mentorId } - ] - }); + const mentorshipDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'mentor', mentorId, 'users') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta); } } diff --git a/__tests__/nested-resolution.ts b/__tests__/nested-resolution.ts index 830fcc9..d6ea7be 100644 --- a/__tests__/nested-resolution.ts +++ b/__tests__/nested-resolution.ts @@ -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,14 +55,10 @@ describe('Nested Object Resolution', () => { }); // Create friendship relationship - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); // Get Alice's lossless view @@ -107,14 +103,10 @@ describe('Nested Object Resolution', () => { // Create user with reference to non-existent friend await userCollection.put('alice', { name: 'Alice' }); - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'nonexistent' } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'nonexistent') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); const aliceViews = node.lossless.view(['alice']); @@ -162,25 +154,17 @@ describe('Nested Object Resolution', () => { await userCollection.put('charlie', { name: 'Charlie' }); // Alice's mentor is Bob - const mentorshipDelta1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'deep-users', target: 'alice', targetContext: 'mentor' }, - { localContext: 'mentor', target: 'bob' } - ] - }); + const mentorshipDelta1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('deep-users', 'alice', 'mentor') + .addPointer('mentor', 'bob') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta1); // Bob's mentor is Charlie - const mentorshipDelta2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'deep-users', target: 'bob', targetContext: 'mentor' }, - { localContext: 'mentor', target: 'charlie' } - ] - }); + const mentorshipDelta2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('deep-users', 'bob', 'mentor') + .addPointer('mentor', 'charlie') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta2); const aliceViews = node.lossless.view(['alice']); @@ -246,24 +230,16 @@ describe('Nested Object Resolution', () => { await userCollection.put('bob', { name: 'Bob' }); // Create circular friendship: Alice -> Bob -> Alice - const friendship1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship1); - const friendship2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'bob', targetContext: 'friends' }, - { localContext: 'friends', target: 'alice' } - ] - }); + const friendship2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'bob', 'friends') + .addPointer('friends', 'alice') + .buildV1(); node.lossless.ingestDelta(friendship2); const aliceViews = node.lossless.view(['alice']); @@ -295,14 +271,10 @@ describe('Nested Object Resolution', () => { await userCollection.put('alice', { name: 'Alice' }); // Alice is friends with herself - const selfFriendship = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'alice' } - ] - }); + const selfFriendship = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'alice') + .buildV1(); node.lossless.ingestDelta(selfFriendship); const aliceViews = node.lossless.view(['alice']); @@ -335,24 +307,16 @@ describe('Nested Object Resolution', () => { await userCollection.put('charlie', { name: 'Charlie' }); // Alice has multiple friends - const friendship1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship1); - const friendship2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'charlie' } - ] - }); + const friendship2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'charlie') + .buildV1(); node.lossless.ingestDelta(friendship2); const aliceViews = node.lossless.view(['alice']); @@ -405,14 +369,10 @@ describe('Nested Object Resolution', () => { }); // Create friendship - const friendship = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship); const aliceViews = node.lossless.view(['alice']); diff --git a/__tests__/query.ts b/__tests__/query.ts index 28a42fc..940ed96 100644 --- a/__tests__/query.ts +++ b/__tests__/query.ts @@ -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 { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { RhizomeNode } from '../src/node'; describe('Query Engine', () => { @@ -48,100 +48,65 @@ describe('Query Engine', () => { async function createUser(id: string, name: string, age?: number, email?: string) { // Create user entity with name - 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 } - ] - }); + const nameDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-name-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'name', name, 'user') + .buildV1(); lossless.ingestDelta(nameDelta); // Add age if provided if (age !== undefined) { - 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 } - ] - }); + const ageDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-age-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'age', age, 'user') + .buildV1(); lossless.ingestDelta(ageDelta); } // Add email if provided if (email) { - 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 } - ] - }); + const emailDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-email-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'email', email, 'user') + .buildV1(); lossless.ingestDelta(emailDelta); } } async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) { // Title delta - 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 } - ] - }); + const titleDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-title-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'title', title, 'post') + .buildV1(); lossless.ingestDelta(titleDelta); // Author delta - 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 } - ] - }); + const authorDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-author-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'author', author, 'post') + .buildV1(); lossless.ingestDelta(authorDelta); // Published delta - 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 } - ] - }); + const publishedDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-published-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'published', published, 'post') + .buildV1(); lossless.ingestDelta(publishedDelta); // Views delta - 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 } - ] - }); + const viewsDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-views-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'views', views, 'post') + .buildV1(); lossless.ingestDelta(viewsDelta); } diff --git a/__tests__/run/001-single-node-orchestrated.ts b/__tests__/run/001-single-node-orchestrated.ts index b75c3e1..a33d6d6 100644 --- a/__tests__/run/001-single-node-orchestrated.ts +++ b/__tests__/run/001-single-node-orchestrated.ts @@ -9,22 +9,12 @@ 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 () => { diff --git a/__tests__/run/002-two-nodes-orchestrated.ts b/__tests__/run/002-two-nodes-orchestrated.ts index 829fa9f..7a00652 100644 --- a/__tests__/run/002-two-nodes-orchestrated.ts +++ b/__tests__/run/002-two-nodes-orchestrated.ts @@ -19,41 +19,26 @@ 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 () => { diff --git a/__tests__/run/005-docker-orchestrator.ts b/__tests__/run/005-docker-orchestrator.ts index 6a94ff4..c2efab4 100644 --- a/__tests__/run/005-docker-orchestrator.ts +++ b/__tests__/run/005-docker-orchestrator.ts @@ -30,7 +30,7 @@ interface ExtendedNodeStatus extends Omit { // Set default timeout for all tests to 5 minutes jest.setTimeout(300000); -describe('Docker Orchestrator V2', () => { +describe('Docker Orchestrator', () => { let docker: Docker; let orchestrator: DockerOrchestrator; let node: NodeHandle | null = null; @@ -43,27 +43,31 @@ describe('Docker Orchestrator V2', () => { beforeAll(async () => { debug('Setting up Docker client and orchestrator...'); - // Initialize Docker client - docker = new Docker(); + // Initialize Docker client with increased timeout + docker = new Docker({ + timeout: 60000, // 60 second timeout for Docker operations + }); // 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 error; + throw new Error(`Docker daemon is not running or not accessible: ${errorMessage}`); } // 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 + // Create a basic node config for testing with unique network ID + const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`; nodePort = 3000 + Math.floor(Math.random() * 1000); nodeConfig = { - id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, - networkId: 'test-network', + id: `node-${testRunId}`, + networkId: `test-network-${testRunId}`, port: nodePort, resources: { memory: 256, // 256MB @@ -79,85 +83,8 @@ describe('Docker Orchestrator V2', () => { afterAll(async () => { debug('Starting test cleanup...'); - const cleanupPromises: Promise[] = []; - - // Helper function to clean up a node with retries - const cleanupNode = async (nodeToClean: NodeHandle | null, nodeName: string) => { - if (!nodeToClean) return; - - debug(`[${nodeName}] Starting cleanup for node ${nodeToClean.id}...`); - try { - // First try the normal stop - await orchestrator.stopNode(nodeToClean).catch(error => { - debug(`[${nodeName}] Warning stopping node normally: %s`, error.message); - throw error; // Will be caught by outer catch - }); - debug(`[${nodeName}] Node ${nodeToClean.id} stopped gracefully`); - } catch (error) { - debug(`[${nodeName}] Error stopping node ${nodeToClean.id}: %o`, error); - - // If normal stop fails, try force cleanup - try { - debug(`[${nodeName}] Attempting force cleanup for node ${nodeToClean.id}...`); - const container = orchestrator.docker.getContainer(`rhizome-${nodeToClean.id}`); - await container.stop({ t: 1 }).catch(() => { - debug(`[${nodeName}] Container stop timed out, forcing removal...`); - }); - await container.remove({ force: true }); - debug(`[${nodeName}] Node ${nodeToClean.id} force-removed`); - } catch (forceError) { - debug(`[${nodeName}] Force cleanup failed for node ${nodeToClean.id}: %o`, forceError); - } - } - }; - - // Clean up all created nodes - if (node) { - cleanupPromises.push(cleanupNode(node, 'node1')); - } - - if (node2) { - cleanupPromises.push(cleanupNode(node2, 'node2')); - } - // Wait for all node cleanups to complete before cleaning up networks - 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); - } + await orchestrator.cleanup(); debug('All test cleanups completed'); }, 120000); // 2 minute timeout for afterAll @@ -166,12 +93,19 @@ describe('Docker Orchestrator V2', () => { 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: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, + id: `node-${testRunId}`, + networkId: `test-network-${testRunId}`, network: { ...nodeConfig.network, enableHttpApi: true + }, + // Add retry logic for Docker operations + docker: { + maxRetries: 3, + retryDelay: 1000 } }; @@ -209,7 +143,7 @@ describe('Docker Orchestrator V2', () => { debug('Error during node cleanup: %o', e); } } - }, 30000); // 30 second timeout for this test + }); it('should enforce resource limits', async () => { debug('Starting test: should enforce resource limits'); diff --git a/__tests__/schema.ts b/__tests__/schema.ts index 5ebc2ff..df18cbf 100644 --- a/__tests__/schema.ts +++ b/__tests__/schema.ts @@ -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 { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; describe('Schema System', () => { let schemaRegistry: DefaultSchemaRegistry; @@ -20,8 +20,10 @@ describe('Schema System', () => { schemaRegistry = new DefaultSchemaRegistry(); node = new RhizomeNode(); }); + describe('Schema Builder', () => { + it('should create a basic schema', () => { const schema = SchemaBuilder .create('user') @@ -308,14 +310,10 @@ describe('Schema System', () => { await collection.put('user2', { name: 'Bob' }); // Manually create an invalid entity by bypassing validation - 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' } - ] - }); + const invalidDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'user3', 'email') + .addPointer('email', 'invalid@test.com') + .buildV1(); node.lossless.ingestDelta(invalidDelta); const stats = collection.getValidationStats(); @@ -337,14 +335,10 @@ describe('Schema System', () => { await collection.put('user2', { name: 'Bob' }); // Create invalid entity manually - 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' } - ] - }); + const invalidDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'user3', 'age') + .addPointer('age', 'not-a-number') + .buildV1(); node.lossless.ingestDelta(invalidDelta); const validIds = collection.getValidEntities(); diff --git a/__tests__/storage.ts b/__tests__/storage.ts index f10e4df..bf58f85 100644 --- a/__tests__/storage.ts +++ b/__tests__/storage.ts @@ -1,39 +1,25 @@ 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 = [ - 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' } - ] - }) + 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() ]; describe('Memory Storage', () => { diff --git a/__tests__/timestamp-resolvers.ts b/__tests__/timestamp-resolvers.ts index 9541600..33c6160 100644 --- a/__tests__/timestamp-resolvers.ts +++ b/__tests__/timestamp-resolvers.ts @@ -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,36 +21,22 @@ describe('Timestamp Resolvers', () => { describe('Basic Timestamp Resolution', () => { test('should resolve by most recent timestamp', () => { // Add older delta - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); // Add newer delta - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host2', - id: 'delta2', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .withId('delta2') + .withTimestamp(2000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new TimestampResolver(lossless); const result = resolver.resolve(); @@ -61,34 +47,20 @@ describe('Timestamp Resolvers', () => { test('should handle multiple entities with different timestamps', () => { // Entity1 - older value - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 100) + .buildV1() + ); // Entity2 - newer value - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "value" - }, { - localContext: "value", - target: 200 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity2', 'value') + .addPointer('value', 200) + .buildV1() + ); const resolver = new TimestampResolver(lossless); const result = resolver.resolve(); @@ -102,35 +74,21 @@ describe('Timestamp Resolvers', () => { describe('Tie-Breaking Strategies', () => { test('should break ties using creator-id strategy', () => { // Two deltas with same timestamp, different creators - 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_z', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .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 - }] - })); + lossless.ingestDelta(createDelta('user_a', 'host1') + .withId('delta2') + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new CreatorIdTimestampResolver(lossless); const result = resolver.resolve(); @@ -142,35 +100,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using delta-id strategy', () => { // Two deltas with same timestamp, different delta IDs - 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_a') // Lexicographically earlier + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .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 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta_z') // Lexicographically later + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new DeltaIdTimestampResolver(lossless); const result = resolver.resolve(); @@ -182,35 +126,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using host-id strategy', () => { // Two deltas with same timestamp, different hosts - 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_z') // Lexicographically later + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .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 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host_a') // Lexicographically earlier + .withId('delta2') + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new HostIdTimestampResolver(lossless); const result = resolver.resolve(); @@ -222,35 +152,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using lexicographic strategy with string values', () => { // Two deltas with same timestamp, different string values - 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('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .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' - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta2') + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'bob') + .buildV1() + ); const resolver = new LexicographicTimestampResolver(lossless); const result = resolver.resolve(); @@ -262,35 +178,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using lexicographic strategy with numeric values (falls back to delta ID)', () => { // Two deltas with same timestamp, numeric values (should fall back to delta ID comparison) - 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_a') // Lexicographically earlier + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 100) + .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 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta_z') // Lexicographically later + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 200) + .buildV1() + ); const resolver = new LexicographicTimestampResolver(lossless); const result = resolver.resolve(); @@ -304,35 +206,21 @@ 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(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_a', 'host1') + .withId('delta_z') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .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' - }] - })); + lossless.ingestDelta(createDelta('user_z', 'host1') + .withId('delta_a') + .withTimestamp(1000) // Same timestamp + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'bob') + .buildV1() + ); const creatorResolver = new CreatorIdTimestampResolver(lossless); const deltaResolver = new DeltaIdTimestampResolver(lossless); @@ -352,36 +240,22 @@ describe('Timestamp Resolvers', () => { test('should work consistently with timestamp priority over tie-breaking', () => { // Add older delta with "better" tie-breaking attributes - 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 - }] - })); + 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() + ); // Add newer delta with "worse" tie-breaking attributes - 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 - }] - })); + 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() + ); const resolver = new CreatorIdTimestampResolver(lossless); const result = resolver.resolve(); @@ -394,20 +268,13 @@ describe('Timestamp Resolvers', () => { describe('Edge Cases', () => { test('should handle single delta correctly', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 42 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 42) + .buildV1() + ); const resolver = new TimestampResolver(lossless, 'creator-id'); const result = resolver.resolve(); @@ -417,35 +284,21 @@ describe('Timestamp Resolvers', () => { }); test('should handle mixed value types correctly', () => { - 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('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'test') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta2', - timeCreated: 1001, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta2') + .withTimestamp(1001) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 100) + .buildV1() + ); const resolver = new TimestampResolver(lossless); const result = resolver.resolve(); diff --git a/__tests__/transactions.ts b/__tests__/transactions.ts index 46c4db3..67ca445 100644 --- a/__tests__/transactions.ts +++ b/__tests__/transactions.ts @@ -1,4 +1,4 @@ -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { Lossless } from '../src/views'; import { RhizomeNode } from '../src/node'; import { DeltaFilter } from '../src/core'; @@ -17,36 +17,23 @@ describe('Transactions', () => { const transactionId = 'tx-123'; // Create a delta that declares a transaction with size 3 - const txDeclaration = new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 3 } - ] - }); + const txDeclaration = createDelta('system', 'host1') + .declareTransaction(transactionId, 3) + .buildV1(); // Create first delta in transaction - 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' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('name', 'user123', 'name') + .addPointer('value', 'Alice') + .buildV1(); // Create second delta in transaction - 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 } - ] - }); + const delta2 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('age', 'user123', 'age') + .addPointer('value', 25) + .buildV1(); // Ingest transaction declaration and first two deltas lossless.ingestDelta(txDeclaration); @@ -58,15 +45,11 @@ describe('Transactions', () => { expect(view.user123).toBeUndefined(); // Add the third delta to complete the transaction - 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' } - ] - }); + const delta3 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('email', 'user123', 'email') + .addPointer('value', 'alice@example.com') + .buildV1(); lossless.ingestDelta(delta3); @@ -83,44 +66,30 @@ describe('Transactions', () => { const tx2 = 'tx-002'; // Declare two transactions - 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(tx1, 2) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: tx2, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(tx2, 2) + .buildV1() + ); // Add deltas for both transactions - 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('user1', 'host1') + .inTransaction(tx1) + .addPointer('status', 'order1', 'status') + .addPointer('value', 'pending') + .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' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(tx2) + .addPointer('status', 'order2', 'status') + .addPointer('value', 'shipped') + .buildV1() + ); // Neither transaction is complete let view = lossless.view(['order1', 'order2']); @@ -128,15 +97,12 @@ describe('Transactions', () => { expect(view.order2).toBeUndefined(); // Complete tx1 - 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 } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .inTransaction(tx1) + .addPointer('total', 'order1', 'total') + .addPointer('value', 100) + .buildV1() + ); // tx1 is complete, tx2 is not view = lossless.view(['order1', 'order2']); @@ -146,15 +112,12 @@ describe('Transactions', () => { expect(view.order2).toBeUndefined(); // Complete tx2 - 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' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(tx2) + .addPointer('tracking', 'order2', 'tracking') + .addPointer('value', 'TRACK123') + .buildV1() + ); // Both transactions complete view = lossless.view(['order1', 'order2']); @@ -168,35 +131,25 @@ describe('Transactions', () => { const transactionId = 'tx-filter-test'; // Create transaction with 2 deltas - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(transactionId, 2) + .buildV1() + ); // Add both deltas - 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('user1', 'host1') + .inTransaction(transactionId) + .addPointer('type', 'doc1', 'type') + .addPointer('value', 'report') + .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' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(transactionId) + .addPointer('author', 'doc1', 'author') + .addPointer('value', 'Bob') + .buildV1() + ); // Create a filter that only accepts deltas from user1 const userFilter: DeltaFilter = (delta) => delta.creator === 'user1'; @@ -215,37 +168,28 @@ describe('Transactions', () => { const transactionId = 'tx-multi-entity'; // Transaction that updates multiple entities atomically - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 3 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 3) + .buildV1() + ); // Transfer money from account1 to account2 - 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') + .addPointer('_transaction', transactionId, 'deltas') + .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: 'account2', targetContext: 'balance' }, - { localContext: 'value', target: 1100 }, - { localContext: 'operation', target: 'credit' } - ] - })); + lossless.ingestDelta(createDelta('bank', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('balance', 'account2', 'balance') + .addPointer('value', 1100) + .addPointer('operation', 'credit') + .buildV1() + ); // Transaction incomplete - no entities should show updates let view = lossless.view(['account1', 'account2']); @@ -253,17 +197,14 @@ describe('Transactions', () => { expect(view.account2).toBeUndefined(); // Complete transaction with audit log - 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 } - ] - })); + lossless.ingestDelta(createDelta('bank', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('transfer', 'transfer123', 'details') + .addPointer('from', 'account1') + .addPointer('to', 'account2') + .addPointer('amount', 100) + .buildV1() + ); // All entities should now be visible view = lossless.view(['account1', 'account2', 'transfer123']); @@ -285,40 +226,29 @@ describe('Transactions', () => { }); // Create transaction - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add first delta - 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' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('field1', 'entity1', 'field1') + .addPointer('value', 'value1') + .buildV1(); lossless.ingestDelta(delta1); // No events should be emitted yet expect(updateEvents).toHaveLength(0); // Add second delta to complete transaction - 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' } - ] - }); + const delta2 = createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('field2', 'entity1', 'field2') + .addPointer('value', 'value2') + .buildV1(); lossless.ingestDelta(delta2); // Wait for async event processing @@ -339,25 +269,19 @@ describe('Transactions', () => { const transactionId = 'tx-wait'; // Create transaction - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add first delta - 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' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('status', 'job1', 'status') + .addPointer('value', 'processing') + .buildV1() + ); // Start waiting for transaction const waitPromise = lossless.transactions.waitFor(transactionId); @@ -369,15 +293,12 @@ describe('Transactions', () => { expect(isResolved).toBe(false); // Complete transaction - 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' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('status', 'job1', 'status') + .addPointer('value', 'completed') + .buildV1() + ); // Wait should now resolve await waitPromise; @@ -391,14 +312,10 @@ describe('Transactions', () => { it('should handle non-transactional deltas normally', () => { // Regular delta without transaction - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'user456', targetContext: 'name' }, - { localContext: 'value', target: 'Charlie' } - ] - }); + const regularDelta = createDelta('user1', 'host1') + .addPointer('name', 'user456', 'name') + .addPointer('value', 'Charlie') + .buildV1(); const updateEvents: string[] = []; lossless.eventStream.on('updated', (entityId) => { @@ -422,33 +339,24 @@ describe('Transactions', () => { const transactionId = 'tx-resize'; // Initially declare transaction with size 2 - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add 2 deltas - 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') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('item1', 'cart1', 'items') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'item2', target: 'cart1', targetContext: 'items' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('item2', 'cart1', 'items') + .buildV1() + ); // Transaction should be complete expect(lossless.transactions.isComplete(transactionId)).toBe(true); @@ -462,15 +370,12 @@ describe('Transactions', () => { const transactionId = 'tx-no-size'; // Add delta with transaction reference but no size declaration - 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' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('data', 'entity1', 'data') + .addPointer('value', 'test') + .buildV1() + ); // Transaction should not be complete (no size) expect(lossless.transactions.isComplete(transactionId)).toBe(false); @@ -480,14 +385,11 @@ describe('Transactions', () => { expect(view.entity1).toBeUndefined(); // Declare size after the fact - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 1 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 1) + .buildV1() + ); // Now transaction should be complete expect(lossless.transactions.isComplete(transactionId)).toBe(true); diff --git a/package.json b/package.json index 0c23c35..421026c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "test": "jest", "coverage": "./scripts/coverage.sh", "coverage-report": "npm run test -- --coverage --coverageDirectory=coverage", - "example-app": "node dist/examples/app.js" + "example-app": "node dist/examples/app.js", + "stop-all": "docker ps -a --filter \"name=^/rhizome-node-\" --format {{.Names}} | xargs -r docker stop", + "build-test-image": "docker build -t rhizome-node-test -f Dockerfile.test ." }, "jest": { "testEnvironment": "node", @@ -35,11 +37,10 @@ "forceExit": true, "verbose": true, "testEnvironmentOptions": { - "NODE_ENV": "test", - "DEBUG": "rz:*" + "NODE_ENV": "test" } }, - "author": "Taliesin (Ladd) ", + "author": "Lentil ", "license": "Unlicense", "dependencies": { "@types/dockerode": "^3.3.40", diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index f08f493..d21dc54 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -134,6 +134,13 @@ export class DeltaBuilder { buildV1(): DeltaV1 { return this.buildV2().toV1(); } + + /** + * Default to V1 for now + */ + build(): DeltaV1 { + return this.buildV1(); + } } /** diff --git a/src/core/index.ts b/src/core/index.ts index 5d09e0f..f8a0681 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,5 @@ export * from './delta'; +export * from './delta-builder'; export * from './types'; export * from './context'; export { Entity } from './entity'; \ No newline at end of file diff --git a/src/orchestration/docker-orchestrator/managers/image-manager.ts b/src/orchestration/docker-orchestrator/managers/image-manager.ts index 5d59802..e1e63ce 100644 --- a/src/orchestration/docker-orchestrator/managers/image-manager.ts +++ b/src/orchestration/docker-orchestrator/managers/image-manager.ts @@ -63,16 +63,11 @@ export class ImageManager implements IImageManager { debug('Created build context tar stream'); testImageBuildPromise = new Promise((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(' '); - const logMessage = `[${timestamp}] ${message}\n`; - process.stdout.write(logMessage); - logMessages.push(logMessage); + debug(message); }; this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => { @@ -143,17 +138,4 @@ export class ImageManager implements IImageManager { }); }); } - - /** - * Check if an image exists locally - */ - async imageExists(imageName: string): Promise { - try { - const image = this.docker.getImage(imageName); - await image.inspect(); - return true; - } catch (error) { - return false; - } - } } diff --git a/src/query/query-engine.ts b/src/query/query-engine.ts index 2fa249b..36a7d58 100644 --- a/src/query/query-engine.ts +++ b/src/query/query-engine.ts @@ -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,9 +268,13 @@ export class QueryEngine { switch (propertySchema.type) { case 'primitive': { // Use last-write-wins for primitives - const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0]; - const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId); - obj[propertyId] = primitiveValue; + 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; + } + } break; } @@ -304,12 +308,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.value !== undefined) { - return pointer.value; + if (pointer[propertyId] !== undefined) { + return pointer[propertyId]; } } return null; diff --git a/src/query/storage-query-engine.ts b/src/query/storage-query-engine.ts index 16f92be..3559ce4 100644 --- a/src/query/storage-query-engine.ts +++ b/src/query/storage-query-engine.ts @@ -263,6 +263,8 @@ export class StorageQueryEngine { default: properties[propertyId] = propDeltas.length; } + + debug(`Resolved property ${propertyId}:`, properties[propertyId]); } return properties; @@ -271,9 +273,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 === 'value') { + if (pointer.localContext === propertyId) { return pointer.target; } } @@ -281,11 +283,11 @@ export class StorageQueryEngine { } /** - * Extract reference value (target ID) from a delta for a given property + * Extract reference value from a delta for a given property */ - private extractReferenceValue(delta: Delta, _propertyId: string): string | null { + private extractReferenceValue(delta: Delta, propertyId: string): string | null { for (const pointer of delta.pointers) { - if (pointer.localContext === 'value' && typeof pointer.target === 'string') { + if (pointer.localContext === propertyId && typeof pointer.target === 'string') { return pointer.target; } } From 795551c623f6f9c80912c0fc973abaf155d2edb6 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Fri, 20 Jun 2025 22:45:10 -0500 Subject: [PATCH 7/7] 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. --- __tests__/delta-builder.test.ts | 10 +-- __tests__/lossless.ts | 6 +- __tests__/negation.ts | 6 +- __tests__/nested-resolution-performance.ts | 23 ++---- __tests__/transactions.ts | 81 +++++++++------------- src/collections/collection-abstract.ts | 34 +++------ src/core/delta-builder.ts | 26 +++---- src/features/negation.ts | 37 ++++------ src/views/lossless.ts | 10 +-- 9 files changed, 83 insertions(+), 150 deletions(-) diff --git a/__tests__/delta-builder.test.ts b/__tests__/delta-builder.test.ts index 95ad931..c8aa404 100644 --- a/__tests__/delta-builder.test.ts +++ b/__tests__/delta-builder.test.ts @@ -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); }); diff --git a/__tests__/lossless.ts b/__tests__/lossless.ts index 65316db..ac39add 100644 --- a/__tests__/lossless.ts +++ b/__tests__/lossless.ts @@ -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() ); diff --git a/__tests__/negation.ts b/__tests__/negation.ts index 28ee328..53e4fa7 100644 --- a/__tests__/negation.ts +++ b/__tests__/negation.ts @@ -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', () => { diff --git a/__tests__/nested-resolution-performance.ts b/__tests__/nested-resolution-performance.ts index e3904c2..382ef61 100644 --- a/__tests__/nested-resolution-performance.ts +++ b/__tests__/nested-resolution-performance.ts @@ -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); } } diff --git a/__tests__/transactions.ts b/__tests__/transactions.ts index 67ca445..12855b5 100644 --- a/__tests__/transactions.ts +++ b/__tests__/transactions.ts @@ -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() ); diff --git a/src/collections/collection-abstract.ts b/src/collections/collection-abstract.ts index 9bf8afc..85feb36 100644 --- a/src/collections/collection-abstract.ts +++ b/src/collections/collection-abstract.ts @@ -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 { 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 { 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) { diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index d21dc54..c4ebce3 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -13,9 +13,6 @@ export class DeltaBuilder { private host: string; private creator: string; private pointers: Record = {}; - 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, diff --git a/src/features/negation.ts b/src/features/negation.ts index 232834b..4f4c30f 100644 --- a/src/features/negation.ts +++ b/src/features/negation.ts @@ -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(); // Create a map of delta ID to its negation deltas - const deltaToNegations = new Map(); + const deltaToNegations = new Map(); // First pass: collect all deltas and their negations for (const delta of deltas) { diff --git a/src/views/lossless.ts b/src/views/lossless.ts index a1ed380..d90f037 100644 --- a/src/views/lossless.ts +++ b/src/views/lossless.ts @@ -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(); - 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);