860 lines
22 KiB
TypeScript
860 lines
22 KiB
TypeScript
/**
|
|
* Transaction Manager
|
|
*
|
|
* Manages distributed transactions with ACID-like semantics
|
|
*/
|
|
|
|
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
|
|
import { Logger, defaultLogger } from '../../core/observability/logger.js';
|
|
import { MetricsCollector, defaultMetricsCollector } from '../../core/observability/metrics.js';
|
|
import { DocumentConflictError } from '../../core/errors/index.js';
|
|
import type {
|
|
TransactionConfig,
|
|
TransactionContext,
|
|
TransactionOperation,
|
|
TransactionResult,
|
|
TransactionStats,
|
|
TransactionState,
|
|
TransactionManagerConfig,
|
|
TransactionCallbacks,
|
|
ConflictInfo,
|
|
ConflictResolutionStrategy,
|
|
Savepoint,
|
|
} from './types.js';
|
|
|
|
/**
|
|
* Default configuration
|
|
*/
|
|
const DEFAULT_CONFIG: Required<TransactionManagerConfig> = {
|
|
defaultIsolationLevel: 'read_committed',
|
|
defaultLockingStrategy: 'optimistic',
|
|
defaultTimeout: 30000, // 30 seconds
|
|
enableCleanup: true,
|
|
cleanupInterval: 60000, // 1 minute
|
|
maxConcurrentTransactions: 1000,
|
|
conflictResolution: 'retry',
|
|
enableLogging: true,
|
|
enableMetrics: true,
|
|
};
|
|
|
|
/**
|
|
* Transaction Manager
|
|
*/
|
|
export class TransactionManager {
|
|
private config: Required<TransactionManagerConfig>;
|
|
private transactions: Map<string, TransactionContext> = new Map();
|
|
private stats: TransactionStats;
|
|
private cleanupTimer?: NodeJS.Timeout;
|
|
private logger: Logger;
|
|
private metrics: MetricsCollector;
|
|
private transactionCounter = 0;
|
|
|
|
constructor(config: TransactionManagerConfig = {}) {
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.logger = defaultLogger;
|
|
this.metrics = defaultMetricsCollector;
|
|
|
|
this.stats = {
|
|
totalStarted: 0,
|
|
totalCommitted: 0,
|
|
totalRolledBack: 0,
|
|
totalFailed: 0,
|
|
totalOperations: 0,
|
|
totalConflicts: 0,
|
|
totalRetries: 0,
|
|
avgDuration: 0,
|
|
avgOperationsPerTransaction: 0,
|
|
successRate: 0,
|
|
activeTransactions: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize transaction manager
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
if (this.config.enableCleanup) {
|
|
this.startCleanupTimer();
|
|
}
|
|
|
|
this.logger.info('TransactionManager initialized', {
|
|
defaultIsolationLevel: this.config.defaultIsolationLevel,
|
|
defaultLockingStrategy: this.config.defaultLockingStrategy,
|
|
maxConcurrentTransactions: this.config.maxConcurrentTransactions,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Begin a new transaction
|
|
*/
|
|
async begin(
|
|
config: TransactionConfig = {},
|
|
callbacks?: TransactionCallbacks
|
|
): Promise<Transaction> {
|
|
// Check concurrent transaction limit
|
|
if (this.transactions.size >= this.config.maxConcurrentTransactions) {
|
|
throw new Error(
|
|
`Maximum concurrent transactions limit reached (${this.config.maxConcurrentTransactions})`
|
|
);
|
|
}
|
|
|
|
// Generate transaction ID
|
|
const transactionId = config.id || this.generateTransactionId();
|
|
|
|
// Create transaction context
|
|
const context: TransactionContext = {
|
|
id: transactionId,
|
|
state: 'active',
|
|
config: {
|
|
id: transactionId,
|
|
isolationLevel: config.isolationLevel ?? this.config.defaultIsolationLevel,
|
|
lockingStrategy: config.lockingStrategy ?? this.config.defaultLockingStrategy,
|
|
timeout: config.timeout ?? this.config.defaultTimeout,
|
|
autoRollback: config.autoRollback ?? true,
|
|
maxRetries: config.maxRetries ?? 3,
|
|
retryDelay: config.retryDelay ?? 100,
|
|
strictOrdering: config.strictOrdering ?? false,
|
|
metadata: config.metadata ?? {},
|
|
},
|
|
operations: [],
|
|
readSet: new Map(),
|
|
writeSet: new Set(),
|
|
startTime: new Date(),
|
|
retryAttempts: 0,
|
|
};
|
|
|
|
this.transactions.set(transactionId, context);
|
|
this.stats.totalStarted++;
|
|
this.stats.activeTransactions++;
|
|
|
|
if (this.config.enableLogging) {
|
|
this.logger.info('Transaction started', {
|
|
transactionId,
|
|
isolationLevel: context.config.isolationLevel,
|
|
lockingStrategy: context.config.lockingStrategy,
|
|
});
|
|
}
|
|
|
|
if (this.config.enableMetrics) {
|
|
this.metrics.recordCounter('transactions.started', 1);
|
|
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
|
}
|
|
|
|
// Call onBegin callback
|
|
if (callbacks?.onBegin) {
|
|
await callbacks.onBegin(context);
|
|
}
|
|
|
|
return new Transaction(this, context, callbacks);
|
|
}
|
|
|
|
/**
|
|
* Get transaction context
|
|
*/
|
|
getTransaction(transactionId: string): TransactionContext | undefined {
|
|
return this.transactions.get(transactionId);
|
|
}
|
|
|
|
/**
|
|
* Commit a transaction
|
|
*/
|
|
async commit(transactionId: string, callbacks?: TransactionCallbacks): Promise<TransactionResult> {
|
|
const context = this.transactions.get(transactionId);
|
|
|
|
if (!context) {
|
|
throw new Error(`Transaction ${transactionId} not found`);
|
|
}
|
|
|
|
if (context.state !== 'active' && context.state !== 'prepared') {
|
|
throw new Error(`Cannot commit transaction in state: ${context.state}`);
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Call onBeforeCommit callback
|
|
if (callbacks?.onBeforeCommit) {
|
|
await callbacks.onBeforeCommit(context);
|
|
}
|
|
|
|
context.state = 'committing';
|
|
|
|
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
|
|
|
// Execute and commit all operations
|
|
let committed = 0;
|
|
for (const operation of context.operations) {
|
|
if (operation.committed) {
|
|
committed++;
|
|
continue;
|
|
}
|
|
|
|
// Execute operation if not yet executed
|
|
if (!operation.executed) {
|
|
await this.executeOperation(context, operation, callbacks);
|
|
}
|
|
|
|
// Mark as committed
|
|
operation.committed = true;
|
|
committed++;
|
|
}
|
|
|
|
context.state = 'committed';
|
|
context.endTime = new Date();
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
this.stats.totalCommitted++;
|
|
this.stats.activeTransactions--;
|
|
this.updateAverages(duration, context.operations.length);
|
|
|
|
const result: TransactionResult = {
|
|
success: true,
|
|
transactionId,
|
|
state: 'committed',
|
|
operationsExecuted: context.operations.filter((op) => op.executed).length,
|
|
operationsCommitted: committed,
|
|
operationsRolledBack: 0,
|
|
duration,
|
|
metadata: context.config.metadata,
|
|
};
|
|
|
|
if (this.config.enableLogging) {
|
|
this.logger.info('Transaction committed', {
|
|
transactionId,
|
|
operations: committed,
|
|
duration,
|
|
});
|
|
}
|
|
|
|
if (this.config.enableMetrics) {
|
|
this.metrics.recordCounter('transactions.committed', 1);
|
|
this.metrics.recordHistogram('transactions.duration', duration);
|
|
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
|
}
|
|
|
|
// Call onAfterCommit callback
|
|
if (callbacks?.onAfterCommit) {
|
|
await callbacks.onAfterCommit(result);
|
|
}
|
|
|
|
// Cleanup transaction
|
|
this.transactions.delete(transactionId);
|
|
|
|
return result;
|
|
} catch (error: any) {
|
|
context.state = 'failed';
|
|
context.error = error;
|
|
|
|
if (this.config.enableLogging) {
|
|
this.logger.error('Transaction commit failed', {
|
|
transactionId,
|
|
error: error.message,
|
|
});
|
|
}
|
|
|
|
// Auto-rollback if enabled
|
|
if (context.config.autoRollback) {
|
|
return await this.rollback(transactionId, callbacks);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rollback a transaction
|
|
*/
|
|
async rollback(
|
|
transactionId: string,
|
|
callbacks?: TransactionCallbacks
|
|
): Promise<TransactionResult> {
|
|
const context = this.transactions.get(transactionId);
|
|
|
|
if (!context) {
|
|
throw new Error(`Transaction ${transactionId} not found`);
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Call onBeforeRollback callback
|
|
if (callbacks?.onBeforeRollback) {
|
|
await callbacks.onBeforeRollback(context);
|
|
}
|
|
|
|
context.state = 'rolling_back';
|
|
|
|
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
|
|
|
// Execute compensation operations in reverse order
|
|
let rolledBack = 0;
|
|
for (let i = context.operations.length - 1; i >= 0; i--) {
|
|
const operation = context.operations[i];
|
|
|
|
if (!operation.executed || !operation.compensation) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await this.executeOperation(context, operation.compensation);
|
|
rolledBack++;
|
|
} catch (error: any) {
|
|
this.logger.error('Compensation operation failed', {
|
|
transactionId,
|
|
operation: operation.type,
|
|
index: operation.index,
|
|
id: operation.id,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
context.state = 'rolled_back';
|
|
context.endTime = new Date();
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
this.stats.totalRolledBack++;
|
|
this.stats.activeTransactions--;
|
|
|
|
const result: TransactionResult = {
|
|
success: false,
|
|
transactionId,
|
|
state: 'rolled_back',
|
|
operationsExecuted: context.operations.filter((op) => op.executed).length,
|
|
operationsCommitted: 0,
|
|
operationsRolledBack: rolledBack,
|
|
duration,
|
|
error: context.error
|
|
? {
|
|
message: context.error.message,
|
|
type: context.error.name,
|
|
}
|
|
: undefined,
|
|
metadata: context.config.metadata,
|
|
};
|
|
|
|
if (this.config.enableLogging) {
|
|
this.logger.info('Transaction rolled back', {
|
|
transactionId,
|
|
rolledBack,
|
|
duration,
|
|
});
|
|
}
|
|
|
|
if (this.config.enableMetrics) {
|
|
this.metrics.recordCounter('transactions.rolled_back', 1);
|
|
this.metrics.recordGauge('transactions.active', this.stats.activeTransactions);
|
|
}
|
|
|
|
// Call onAfterRollback callback
|
|
if (callbacks?.onAfterRollback) {
|
|
await callbacks.onAfterRollback(result);
|
|
}
|
|
|
|
// Cleanup transaction
|
|
this.transactions.delete(transactionId);
|
|
|
|
return result;
|
|
} catch (error: any) {
|
|
context.state = 'failed';
|
|
context.error = error;
|
|
this.stats.totalFailed++;
|
|
|
|
if (this.config.enableLogging) {
|
|
this.logger.error('Transaction rollback failed', {
|
|
transactionId,
|
|
error: error.message,
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get transaction statistics
|
|
*/
|
|
getStats(): TransactionStats {
|
|
return { ...this.stats };
|
|
}
|
|
|
|
/**
|
|
* Destroy transaction manager
|
|
*/
|
|
async destroy(): Promise<void> {
|
|
if (this.cleanupTimer) {
|
|
clearInterval(this.cleanupTimer);
|
|
}
|
|
|
|
// Rollback all active transactions
|
|
const activeTransactions = Array.from(this.transactions.keys());
|
|
for (const transactionId of activeTransactions) {
|
|
try {
|
|
await this.rollback(transactionId);
|
|
} catch (error) {
|
|
// Ignore errors during cleanup
|
|
}
|
|
}
|
|
|
|
this.transactions.clear();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal Methods
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Add operation to transaction
|
|
*/
|
|
addOperation(context: TransactionContext, operation: TransactionOperation): void {
|
|
context.operations.push(operation);
|
|
this.stats.totalOperations++;
|
|
|
|
const key = `${operation.index}:${operation.id}`;
|
|
|
|
if (operation.type === 'read') {
|
|
// Add to read set for repeatable read
|
|
if (operation.version) {
|
|
context.readSet.set(key, operation.version);
|
|
}
|
|
} else {
|
|
// Add to write set
|
|
context.writeSet.add(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute an operation
|
|
*/
|
|
private async executeOperation(
|
|
context: TransactionContext,
|
|
operation: TransactionOperation,
|
|
callbacks?: TransactionCallbacks
|
|
): Promise<void> {
|
|
// Call onBeforeOperation callback
|
|
if (callbacks?.onBeforeOperation) {
|
|
await callbacks.onBeforeOperation(operation);
|
|
}
|
|
|
|
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
|
|
|
try {
|
|
switch (operation.type) {
|
|
case 'read': {
|
|
const result = await client.get({
|
|
index: operation.index,
|
|
id: operation.id,
|
|
});
|
|
|
|
operation.version = {
|
|
seqNo: result._seq_no!,
|
|
primaryTerm: result._primary_term!,
|
|
};
|
|
|
|
operation.originalDocument = result._source;
|
|
break;
|
|
}
|
|
|
|
case 'create': {
|
|
const result = await client.index({
|
|
index: operation.index,
|
|
id: operation.id,
|
|
document: operation.document,
|
|
op_type: 'create',
|
|
});
|
|
|
|
operation.version = {
|
|
seqNo: result._seq_no,
|
|
primaryTerm: result._primary_term,
|
|
};
|
|
|
|
// Create compensation (delete)
|
|
operation.compensation = {
|
|
type: 'delete',
|
|
index: operation.index,
|
|
id: operation.id,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
break;
|
|
}
|
|
|
|
case 'update': {
|
|
const updateRequest: any = {
|
|
index: operation.index,
|
|
id: operation.id,
|
|
document: operation.document,
|
|
};
|
|
|
|
// Add version for optimistic locking
|
|
if (operation.version) {
|
|
updateRequest.if_seq_no = operation.version.seqNo;
|
|
updateRequest.if_primary_term = operation.version.primaryTerm;
|
|
}
|
|
|
|
const result = await client.index(updateRequest);
|
|
|
|
operation.version = {
|
|
seqNo: result._seq_no,
|
|
primaryTerm: result._primary_term,
|
|
};
|
|
|
|
// Create compensation (restore original)
|
|
if (operation.originalDocument) {
|
|
operation.compensation = {
|
|
type: 'update',
|
|
index: operation.index,
|
|
id: operation.id,
|
|
document: operation.originalDocument,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'delete': {
|
|
const deleteRequest: any = {
|
|
index: operation.index,
|
|
id: operation.id,
|
|
};
|
|
|
|
// Add version for optimistic locking
|
|
if (operation.version) {
|
|
deleteRequest.if_seq_no = operation.version.seqNo;
|
|
deleteRequest.if_primary_term = operation.version.primaryTerm;
|
|
}
|
|
|
|
await client.delete(deleteRequest);
|
|
|
|
// Create compensation (restore document)
|
|
if (operation.originalDocument) {
|
|
operation.compensation = {
|
|
type: 'create',
|
|
index: operation.index,
|
|
id: operation.id,
|
|
document: operation.originalDocument,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
operation.executed = true;
|
|
|
|
// Call onAfterOperation callback
|
|
if (callbacks?.onAfterOperation) {
|
|
await callbacks.onAfterOperation(operation);
|
|
}
|
|
} catch (error: any) {
|
|
// Handle version conflict
|
|
if (error.name === 'ResponseError' && error.meta?.statusCode === 409) {
|
|
await this.handleConflict(context, operation, error, callbacks);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle version conflict
|
|
*/
|
|
private async handleConflict(
|
|
context: TransactionContext,
|
|
operation: TransactionOperation,
|
|
error: Error,
|
|
callbacks?: TransactionCallbacks
|
|
): Promise<void> {
|
|
this.stats.totalConflicts++;
|
|
|
|
const conflict: ConflictInfo = {
|
|
operation,
|
|
expectedVersion: operation.version,
|
|
detectedAt: new Date(),
|
|
};
|
|
|
|
if (this.config.enableMetrics) {
|
|
this.metrics.recordCounter('transactions.conflicts', 1);
|
|
}
|
|
|
|
// Call onConflict callback
|
|
let strategy: ConflictResolutionStrategy = this.config.conflictResolution;
|
|
if (callbacks?.onConflict) {
|
|
strategy = await callbacks.onConflict(conflict);
|
|
}
|
|
|
|
switch (strategy) {
|
|
case 'abort':
|
|
throw new DocumentConflictError(
|
|
`Version conflict for ${operation.index}/${operation.id}`,
|
|
{ index: operation.index, id: operation.id }
|
|
);
|
|
|
|
case 'retry':
|
|
if (context.retryAttempts >= context.config.maxRetries) {
|
|
throw new DocumentConflictError(
|
|
`Max retries exceeded for ${operation.index}/${operation.id}`,
|
|
{ index: operation.index, id: operation.id }
|
|
);
|
|
}
|
|
|
|
context.retryAttempts++;
|
|
this.stats.totalRetries++;
|
|
|
|
// Wait before retry
|
|
await new Promise((resolve) => setTimeout(resolve, context.config.retryDelay));
|
|
|
|
// Retry operation
|
|
await this.executeOperation(context, operation, callbacks);
|
|
break;
|
|
|
|
case 'skip':
|
|
// Skip this operation
|
|
operation.executed = false;
|
|
break;
|
|
|
|
case 'force':
|
|
// Force update without version check
|
|
delete operation.version;
|
|
await this.executeOperation(context, operation, callbacks);
|
|
break;
|
|
|
|
case 'merge':
|
|
// Not implemented - requires custom merge logic
|
|
throw new Error('Merge conflict resolution not implemented');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate transaction ID
|
|
*/
|
|
private generateTransactionId(): string {
|
|
this.transactionCounter++;
|
|
return `txn-${Date.now()}-${this.transactionCounter}`;
|
|
}
|
|
|
|
/**
|
|
* Start cleanup timer for expired transactions
|
|
*/
|
|
private startCleanupTimer(): void {
|
|
this.cleanupTimer = setInterval(() => {
|
|
this.cleanupExpiredTransactions();
|
|
}, this.config.cleanupInterval);
|
|
}
|
|
|
|
/**
|
|
* Cleanup expired transactions
|
|
*/
|
|
private cleanupExpiredTransactions(): void {
|
|
const now = Date.now();
|
|
|
|
for (const [transactionId, context] of this.transactions) {
|
|
const elapsed = now - context.startTime.getTime();
|
|
|
|
if (elapsed > context.config.timeout) {
|
|
this.logger.warn('Transaction timeout, rolling back', { transactionId });
|
|
|
|
this.rollback(transactionId).catch((error) => {
|
|
this.logger.error('Failed to rollback expired transaction', {
|
|
transactionId,
|
|
error,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update average statistics
|
|
*/
|
|
private updateAverages(duration: number, operations: number): void {
|
|
const total = this.stats.totalCommitted + this.stats.totalRolledBack;
|
|
|
|
this.stats.avgDuration =
|
|
(this.stats.avgDuration * (total - 1) + duration) / total;
|
|
|
|
this.stats.avgOperationsPerTransaction =
|
|
(this.stats.avgOperationsPerTransaction * (total - 1) + operations) / total;
|
|
|
|
this.stats.successRate =
|
|
this.stats.totalCommitted / (this.stats.totalCommitted + this.stats.totalRolledBack + this.stats.totalFailed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transaction class for fluent API
|
|
*/
|
|
export class Transaction {
|
|
private savepoints: Map<string, Savepoint> = new Map();
|
|
|
|
constructor(
|
|
private manager: TransactionManager,
|
|
private context: TransactionContext,
|
|
private callbacks?: TransactionCallbacks
|
|
) {}
|
|
|
|
/**
|
|
* Get transaction ID
|
|
*/
|
|
getId(): string {
|
|
return this.context.id;
|
|
}
|
|
|
|
/**
|
|
* Get transaction state
|
|
*/
|
|
getState(): TransactionState {
|
|
return this.context.state;
|
|
}
|
|
|
|
/**
|
|
* Read a document
|
|
*/
|
|
async read<T>(index: string, id: string): Promise<T | null> {
|
|
const operation: TransactionOperation<T> = {
|
|
type: 'read',
|
|
index,
|
|
id,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
|
|
this.manager.addOperation(this.context, operation);
|
|
|
|
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
|
|
|
try {
|
|
const result = await client.get({ index, id });
|
|
|
|
operation.version = {
|
|
seqNo: result._seq_no!,
|
|
primaryTerm: result._primary_term!,
|
|
};
|
|
|
|
operation.originalDocument = result._source as T;
|
|
operation.executed = true;
|
|
|
|
return result._source as T;
|
|
} catch (error: any) {
|
|
if (error.name === 'ResponseError' && error.meta?.statusCode === 404) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a document
|
|
*/
|
|
async create<T>(index: string, id: string, document: T): Promise<void> {
|
|
const operation: TransactionOperation<T> = {
|
|
type: 'create',
|
|
index,
|
|
id,
|
|
document,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
|
|
this.manager.addOperation(this.context, operation);
|
|
}
|
|
|
|
/**
|
|
* Update a document
|
|
*/
|
|
async update<T>(index: string, id: string, document: Partial<T>): Promise<void> {
|
|
// First read the current version
|
|
const current = await this.read<T>(index, id);
|
|
|
|
const operation: TransactionOperation<T> = {
|
|
type: 'update',
|
|
index,
|
|
id,
|
|
document: { ...current, ...document } as T,
|
|
originalDocument: current ?? undefined,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
|
|
this.manager.addOperation(this.context, operation);
|
|
}
|
|
|
|
/**
|
|
* Delete a document
|
|
*/
|
|
async delete(index: string, id: string): Promise<void> {
|
|
// First read the current version
|
|
const current = await this.read(index, id);
|
|
|
|
const operation: TransactionOperation = {
|
|
type: 'delete',
|
|
index,
|
|
id,
|
|
originalDocument: current ?? undefined,
|
|
timestamp: new Date(),
|
|
executed: false,
|
|
committed: false,
|
|
};
|
|
|
|
this.manager.addOperation(this.context, operation);
|
|
}
|
|
|
|
/**
|
|
* Create a savepoint
|
|
*/
|
|
savepoint(name: string): void {
|
|
this.savepoints.set(name, {
|
|
name,
|
|
operationsCount: this.context.operations.length,
|
|
createdAt: new Date(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Rollback to savepoint
|
|
*/
|
|
rollbackTo(name: string): void {
|
|
const savepoint = this.savepoints.get(name);
|
|
|
|
if (!savepoint) {
|
|
throw new Error(`Savepoint '${name}' not found`);
|
|
}
|
|
|
|
// Remove operations after savepoint
|
|
this.context.operations.splice(savepoint.operationsCount);
|
|
}
|
|
|
|
/**
|
|
* Commit the transaction
|
|
*/
|
|
async commit(): Promise<TransactionResult> {
|
|
return await this.manager.commit(this.context.id, this.callbacks);
|
|
}
|
|
|
|
/**
|
|
* Rollback the transaction
|
|
*/
|
|
async rollback(): Promise<TransactionResult> {
|
|
return await this.manager.rollback(this.context.id, this.callbacks);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a transaction manager
|
|
*/
|
|
export function createTransactionManager(
|
|
config?: TransactionManagerConfig
|
|
): TransactionManager {
|
|
return new TransactionManager(config);
|
|
}
|