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);