rhizome-node/__tests__/unit/core/transactions.test.ts
Lentil Hoffman d7c4fda93e
refactor(resolver): overhaul plugin system and dependency handling
Core Changes:
- Completely rewrote CustomResolver reducer with dependency-ordered processing
- Enhanced plugin initialization with proper dependency injection
- Improved delta processing and property value tracking
- Added robust error handling for duplicate property IDs

Resolver Improvements:
- Updated to use new accumulator structure
- Implemented execution order processing for plugins
- Enhanced debug logging and error reporting
- Simplified TimestampResolver by removing unused initializer

Configuration Updates:
- Added TypeScript path aliases for test helpers
- Improved module resolution paths

Key Benefits:
- More robust plugin dependency management
- More efficient state updates
- Enhanced type safety
- Better error messages and debugging
- More consistent plugin initialization

This refactoring focuses on improving the robustness of the resolver,
especially around plugin lifecycle management and dependency handling.
The changes ensure better separation of concerns and more predictable
behavior when dealing with complex plugin dependencies.
2025-06-25 06:10:34 -05:00

383 lines
13 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', () => {
test('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)
.setProperty('user123', 'name', 'Alice')
.buildV1();
// Create second delta in transaction
const delta2 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('user123', 'age', 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.compose(['user123']);
expect(view.user123).toBeUndefined();
// Add the third delta to complete the transaction
const delta3 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('user123', 'email', 'alice@example.com')
.buildV1();
lossless.ingestDelta(delta3);
// Now the view should include all deltas from the completed transaction
const completeView = lossless.compose(['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);
});
test('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)
.setProperty('order1', 'status', 'pending')
.buildV1()
);
lossless.ingestDelta(createDelta('user2', 'host2')
.inTransaction(tx2)
.setProperty('order2', 'status', 'shipped')
.buildV1()
);
// Neither transaction is complete
let view = lossless.compose(['order1', 'order2']);
expect(view.order1).toBeUndefined();
expect(view.order2).toBeUndefined();
// Complete tx1
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(tx1)
.setProperty('order1', 'total', 100)
.buildV1()
);
// tx1 is complete, tx2 is not
view = lossless.compose(['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)
.setProperty('order2', 'tracking', 'TRACK123')
.buildV1()
);
// Both transactions complete
view = lossless.compose(['order1', 'order2']);
expect(view.order1).toBeDefined();
expect(view.order2).toBeDefined();
expect(view.order2.propertyDeltas.status).toHaveLength(1);
expect(view.order2.propertyDeltas.tracking).toHaveLength(1);
});
test('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)
.setProperty('doc1', 'type', 'report')
.buildV1()
);
lossless.ingestDelta(createDelta('user2', 'host2')
.inTransaction(transactionId)
.setProperty('doc1', 'author', '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.compose(['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();
});
test('should handle transaction with deltas affecting multiple entities', () => {
const transactionId = 'tx-multi-entity';
// Transaction that updates multiple entities atomically
lossless.ingestDelta(createDelta('system', 'host1')
.declareTransaction(transactionId, 3)
.buildV1()
);
// Transfer money from account1 to account2
lossless.ingestDelta(createDelta('bank', 'host1')
.inTransaction(transactionId)
.addPointer('balance', 'account1', 'balance')
.addPointer('value', 900)
.addPointer('operation', 'debit')
.buildV1()
);
lossless.ingestDelta(createDelta('bank', 'host1')
.inTransaction(transactionId)
.addPointer('balance', 'account2', 'balance')
.addPointer('value', 1100)
.addPointer('operation', 'credit')
.buildV1()
);
// Transaction incomplete - no entities should show updates
let view = lossless.compose(['account1', 'account2']);
expect(view.account1).toBeUndefined();
expect(view.account2).toBeUndefined();
// Complete transaction with audit log
lossless.ingestDelta(createDelta('bank', 'host1')
.inTransaction(transactionId)
.addPointer('transfer', 'transfer123', 'details')
.addPointer('from', 'account1')
.addPointer('to', 'account2')
.addPointer('amount', 100)
.buildV1()
);
// All entities should now be visible
view = lossless.compose(['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);
});
test('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: string, deltaIds: string[]) => {
updateEvents.push({ entityId, deltaIds });
});
// Create transaction
lossless.ingestDelta(createDelta('system', 'host1')
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add first delta
const delta1 = createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('entity1', 'field1', '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')
.inTransaction(transactionId)
.setProperty('entity1', 'field2', '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);
});
test('should support waiting for transaction completion', async () => {
const transactionId = 'tx-wait';
// Create transaction
lossless.ingestDelta(createDelta('system', 'host1')
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add first delta
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('job1', 'status', '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')
.inTransaction(transactionId)
.setProperty('job1', 'status', 'completed')
.buildV1()
);
// Wait should now resolve
await waitPromise;
expect(isResolved).toBe(true);
// View should show completed transaction
const view = lossless.compose(['job1']);
expect(view.job1).toBeDefined();
expect(view.job1.propertyDeltas.status).toHaveLength(2);
});
test('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: string) => {
updateEvents.push(entityId);
});
lossless.ingestDelta(regularDelta);
// Should immediately appear in view
const view = lossless.compose(['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', () => {
test('should handle transaction size updates', () => {
const transactionId = 'tx-resize';
// Initially declare transaction with size 2
lossless.ingestDelta(createDelta('system', 'host1')
.declareTransaction(transactionId, 2)
.buildV1()
);
// Add 2 deltas
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('cart1', 'items', 'item1')
.buildV1()
);
lossless.ingestDelta(createDelta('user1', 'host1')
.inTransaction(transactionId)
.setProperty('cart1', 'items', 'item2')
.buildV1()
);
// Transaction should be complete
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
// View should show the cart
const view = lossless.compose(['cart1']);
expect(view.cart1).toBeDefined();
});
test('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')
.inTransaction(transactionId)
.setProperty('entity1', 'data', '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.compose(['entity1']);
expect(view.entity1).toBeUndefined();
// Declare size after the fact
lossless.ingestDelta(createDelta('system', 'host1')
.declareTransaction(transactionId, 1)
.buildV1()
);
// Now transaction should be complete
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
// And delta should appear in view
const viewAfter = lossless.compose(['entity1']);
expect(viewAfter.entity1).toBeDefined();
});
});
});