- 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.
402 lines
14 KiB
TypeScript
402 lines
14 KiB
TypeScript
import { createDelta } from '../src/core/delta-builder';
|
|
import { Lossless } from '../src/views';
|
|
import { RhizomeNode } from '../src/node';
|
|
import { DeltaFilter } from '../src/core';
|
|
|
|
describe('Transactions', () => {
|
|
let node: RhizomeNode;
|
|
let lossless: Lossless;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
lossless = new Lossless(node);
|
|
});
|
|
|
|
describe('Transaction-based filtering', () => {
|
|
it('should exclude deltas from incomplete transactions', () => {
|
|
const transactionId = 'tx-123';
|
|
|
|
// Create a delta that declares a transaction with size 3
|
|
const txDeclaration = createDelta('system', 'host1')
|
|
.declareTransaction(transactionId, 3)
|
|
.buildV1();
|
|
|
|
// Create first delta in transaction
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.inTransaction(transactionId)
|
|
.addPointer('name', 'user123', 'name')
|
|
.addPointer('value', 'Alice')
|
|
.buildV1();
|
|
|
|
// Create second delta in transaction
|
|
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);
|
|
lossless.ingestDelta(delta1);
|
|
lossless.ingestDelta(delta2);
|
|
|
|
// View should be empty because transaction is incomplete (2/3 deltas)
|
|
const view = lossless.view(['user123']);
|
|
expect(view.user123).toBeUndefined();
|
|
|
|
// Add the third delta to complete the transaction
|
|
const delta3 = createDelta('user1', 'host1')
|
|
.inTransaction(transactionId)
|
|
.addPointer('email', 'user123', 'email')
|
|
.addPointer('value', 'alice@example.com')
|
|
.buildV1();
|
|
|
|
lossless.ingestDelta(delta3);
|
|
|
|
// Now the view should include all deltas from the completed transaction
|
|
const completeView = lossless.view(['user123']);
|
|
expect(completeView.user123).toBeDefined();
|
|
expect(completeView.user123.propertyDeltas.name).toHaveLength(1);
|
|
expect(completeView.user123.propertyDeltas.age).toHaveLength(1);
|
|
expect(completeView.user123.propertyDeltas.email).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle multiple transactions independently', () => {
|
|
const tx1 = 'tx-001';
|
|
const tx2 = 'tx-002';
|
|
|
|
// Declare two transactions
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.declareTransaction(tx1, 2)
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.declareTransaction(tx2, 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add deltas for both transactions
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.inTransaction(tx1)
|
|
.addPointer('status', 'order1', 'status')
|
|
.addPointer('value', 'pending')
|
|
.buildV1()
|
|
);
|
|
|
|
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']);
|
|
expect(view.order1).toBeUndefined();
|
|
expect(view.order2).toBeUndefined();
|
|
|
|
// Complete tx1
|
|
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']);
|
|
expect(view.order1).toBeDefined();
|
|
expect(view.order1.propertyDeltas.status).toHaveLength(1);
|
|
expect(view.order1.propertyDeltas.total).toHaveLength(1);
|
|
expect(view.order2).toBeUndefined();
|
|
|
|
// Complete tx2
|
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
|
.inTransaction(tx2)
|
|
.addPointer('tracking', 'order2', 'tracking')
|
|
.addPointer('value', 'TRACK123')
|
|
.buildV1()
|
|
);
|
|
|
|
// Both transactions complete
|
|
view = lossless.view(['order1', 'order2']);
|
|
expect(view.order1).toBeDefined();
|
|
expect(view.order2).toBeDefined();
|
|
expect(view.order2.propertyDeltas.status).toHaveLength(1);
|
|
expect(view.order2.propertyDeltas.tracking).toHaveLength(1);
|
|
});
|
|
|
|
it('should work with transaction-aware delta filters', () => {
|
|
const transactionId = 'tx-filter-test';
|
|
|
|
// Create transaction with 2 deltas
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.declareTransaction(transactionId, 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add both deltas
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.inTransaction(transactionId)
|
|
.addPointer('type', 'doc1', 'type')
|
|
.addPointer('value', 'report')
|
|
.buildV1()
|
|
);
|
|
|
|
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';
|
|
|
|
// With incomplete transaction, nothing should show
|
|
// But once complete, the filter should still apply
|
|
const view = lossless.view(['doc1'], userFilter);
|
|
|
|
// Even though transaction is complete, only delta from user1 should appear
|
|
expect(view.doc1).toBeDefined();
|
|
expect(view.doc1.propertyDeltas.type).toHaveLength(1);
|
|
expect(view.doc1.propertyDeltas.author).toBeUndefined();
|
|
});
|
|
|
|
it('should handle transaction with deltas affecting multiple entities', () => {
|
|
const transactionId = 'tx-multi-entity';
|
|
|
|
// Transaction that updates multiple entities atomically
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.addPointer('_transaction', transactionId, 'size')
|
|
.addPointer('size', 3)
|
|
.buildV1()
|
|
);
|
|
|
|
// Transfer money from account1 to account2
|
|
lossless.ingestDelta(createDelta('bank', 'host1')
|
|
.addPointer('_transaction', transactionId, 'deltas')
|
|
.addPointer('balance', 'account1', 'balance')
|
|
.addPointer('value', 900)
|
|
.addPointer('operation', 'debit')
|
|
.buildV1()
|
|
);
|
|
|
|
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']);
|
|
expect(view.account1).toBeUndefined();
|
|
expect(view.account2).toBeUndefined();
|
|
|
|
// Complete transaction with audit log
|
|
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']);
|
|
expect(view.account1).toBeDefined();
|
|
expect(view.account1.propertyDeltas.balance).toHaveLength(1);
|
|
expect(view.account2).toBeDefined();
|
|
expect(view.account2.propertyDeltas.balance).toHaveLength(1);
|
|
expect(view.transfer123).toBeDefined();
|
|
expect(view.transfer123.propertyDeltas.details).toHaveLength(1);
|
|
});
|
|
|
|
it('should emit events only when transactions complete', async () => {
|
|
const transactionId = 'tx-events';
|
|
const updateEvents: Array<{ entityId: string, deltaIds: string[] }> = [];
|
|
|
|
// Listen for update events
|
|
lossless.eventStream.on('updated', (entityId, deltaIds) => {
|
|
updateEvents.push({ entityId, deltaIds });
|
|
});
|
|
|
|
// Create transaction
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.addPointer('_transaction', transactionId, 'size')
|
|
.addPointer('size', 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add first delta
|
|
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 = createDelta('user1', 'host1')
|
|
.addPointer('_transaction', transactionId, 'deltas')
|
|
.addPointer('field2', 'entity1', 'field2')
|
|
.addPointer('value', 'value2')
|
|
.buildV1();
|
|
lossless.ingestDelta(delta2);
|
|
|
|
// Wait for async event processing
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
// Now we should have received update events
|
|
// One for the transaction entity itself, and one for entity1
|
|
expect(updateEvents).toHaveLength(2);
|
|
|
|
// Find the entity1 update event
|
|
const entity1Update = updateEvents.find(e => e.entityId === 'entity1');
|
|
expect(entity1Update).toBeDefined();
|
|
expect(entity1Update!.deltaIds).toContain(delta1.id);
|
|
expect(entity1Update!.deltaIds).toContain(delta2.id);
|
|
});
|
|
|
|
it('should support waiting for transaction completion', async () => {
|
|
const transactionId = 'tx-wait';
|
|
|
|
// Create transaction
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.addPointer('_transaction', transactionId, 'size')
|
|
.addPointer('size', 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add first delta
|
|
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);
|
|
let isResolved = false;
|
|
waitPromise.then(() => { isResolved = true; });
|
|
|
|
// Should not be resolved yet
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
expect(isResolved).toBe(false);
|
|
|
|
// Complete transaction
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.addPointer('_transaction', transactionId, 'deltas')
|
|
.addPointer('status', 'job1', 'status')
|
|
.addPointer('value', 'completed')
|
|
.buildV1()
|
|
);
|
|
|
|
// Wait should now resolve
|
|
await waitPromise;
|
|
expect(isResolved).toBe(true);
|
|
|
|
// View should show completed transaction
|
|
const view = lossless.view(['job1']);
|
|
expect(view.job1).toBeDefined();
|
|
expect(view.job1.propertyDeltas.status).toHaveLength(2);
|
|
});
|
|
|
|
it('should handle non-transactional deltas normally', () => {
|
|
// Regular delta without transaction
|
|
const regularDelta = createDelta('user1', 'host1')
|
|
.addPointer('name', 'user456', 'name')
|
|
.addPointer('value', 'Charlie')
|
|
.buildV1();
|
|
|
|
const updateEvents: string[] = [];
|
|
lossless.eventStream.on('updated', (entityId) => {
|
|
updateEvents.push(entityId);
|
|
});
|
|
|
|
lossless.ingestDelta(regularDelta);
|
|
|
|
// Should immediately appear in view
|
|
const view = lossless.view(['user456']);
|
|
expect(view.user456).toBeDefined();
|
|
expect(view.user456.propertyDeltas.name).toHaveLength(1);
|
|
|
|
// Should immediately emit update event
|
|
expect(updateEvents).toContain('user456');
|
|
});
|
|
});
|
|
|
|
describe('Transaction edge cases', () => {
|
|
it('should handle transaction size updates', () => {
|
|
const transactionId = 'tx-resize';
|
|
|
|
// Initially declare transaction with size 2
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.addPointer('_transaction', transactionId, 'size')
|
|
.addPointer('size', 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add 2 deltas
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.addPointer('_transaction', transactionId, 'deltas')
|
|
.addPointer('item1', 'cart1', 'items')
|
|
.buildV1()
|
|
);
|
|
|
|
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);
|
|
|
|
// View should show the cart
|
|
const view = lossless.view(['cart1']);
|
|
expect(view.cart1).toBeDefined();
|
|
});
|
|
|
|
it('should handle missing transaction size gracefully', () => {
|
|
const transactionId = 'tx-no-size';
|
|
|
|
// 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')
|
|
.buildV1()
|
|
);
|
|
|
|
// Transaction should not be complete (no size)
|
|
expect(lossless.transactions.isComplete(transactionId)).toBe(false);
|
|
|
|
// Delta should not appear in view
|
|
const view = lossless.view(['entity1']);
|
|
expect(view.entity1).toBeUndefined();
|
|
|
|
// Declare size after the fact
|
|
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);
|
|
|
|
// And delta should appear in view
|
|
const viewAfter = lossless.view(['entity1']);
|
|
expect(viewAfter.entity1).toBeDefined();
|
|
});
|
|
});
|
|
}); |