feat: add v5.8.0 features - transactions, pagination, and comprehensive docs
**Transaction System (TIER 1.2)** - Atomic operations with automatic rollback - 36 unit tests + 35 integration tests passing - Full documentation in docs/transactions.md **Duplicate Check Optimization (TIER 1.4)** - Optimized from O(n) to O(log n) using GraphAdjacencyIndex - Uses LSM-tree for efficient lookups - Tests verify performance improvements **GraphIndex Pagination (TIER 1.5)** - Production-scale pagination for high-degree nodes - Backward compatible API - 18 pagination tests passing **Comprehensive Filter Documentation (TIER 1.6)** - Complete operator reference (15 operators) - Compound filters (anyOf, allOf, nested logic) - Common query patterns and troubleshooting guide - 642 lines of new documentation **README Updates** - Added Filter & Query Syntax Guide to Essential Reading - Added Transactions to Core Concepts section All changes tested and production-ready for v5.8.0 release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
52e961760d
commit
e40fee39d8
21 changed files with 5657 additions and 182 deletions
238
src/transaction/Transaction.ts
Normal file
238
src/transaction/Transaction.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
/**
|
||||
* Transaction - Atomic Unit of Work
|
||||
*
|
||||
* Executes operations atomically: all succeed or all rollback.
|
||||
* Prevents partial failures that leave system in inconsistent state.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const tx = new Transaction()
|
||||
* tx.addOperation(operation1)
|
||||
* tx.addOperation(operation2)
|
||||
* await tx.execute() // Both succeed or both rollback
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
Operation,
|
||||
RollbackAction,
|
||||
TransactionState,
|
||||
TransactionContext,
|
||||
TransactionOptions
|
||||
} from './types.js'
|
||||
import {
|
||||
InvalidTransactionStateError,
|
||||
TransactionExecutionError,
|
||||
TransactionRollbackError,
|
||||
TransactionTimeoutError
|
||||
} from './errors.js'
|
||||
import { prodLog } from '../utils/logger.js'
|
||||
|
||||
/**
|
||||
* Default transaction options
|
||||
*/
|
||||
const DEFAULT_OPTIONS: Required<TransactionOptions> = {
|
||||
timeout: 30000, // 30 seconds
|
||||
logging: false,
|
||||
maxRollbackRetries: 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction class
|
||||
*/
|
||||
export class Transaction implements TransactionContext {
|
||||
private operations: Operation[] = []
|
||||
private rollbackActions: RollbackAction[] = []
|
||||
private state: TransactionState = 'pending'
|
||||
private readonly options: Required<TransactionOptions>
|
||||
private startTime?: number
|
||||
private endTime?: number
|
||||
|
||||
constructor(options: TransactionOptions = {}) {
|
||||
this.options = { ...DEFAULT_OPTIONS, ...options }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an operation to the transaction
|
||||
*/
|
||||
addOperation(operation: Operation): void {
|
||||
if (this.state !== 'pending') {
|
||||
throw new InvalidTransactionStateError(
|
||||
this.state,
|
||||
'add operation'
|
||||
)
|
||||
}
|
||||
this.operations.push(operation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current transaction state
|
||||
*/
|
||||
getState(): TransactionState {
|
||||
return this.state
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of operations in transaction
|
||||
*/
|
||||
getOperationCount(): number {
|
||||
return this.operations.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all operations atomically
|
||||
*/
|
||||
async execute(): Promise<void> {
|
||||
if (this.state !== 'pending') {
|
||||
throw new InvalidTransactionStateError(
|
||||
this.state,
|
||||
'execute'
|
||||
)
|
||||
}
|
||||
|
||||
this.state = 'executing'
|
||||
this.startTime = Date.now()
|
||||
|
||||
if (this.options.logging) {
|
||||
prodLog.info(`[Transaction] Executing ${this.operations.length} operations`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute each operation in order
|
||||
for (let i = 0; i < this.operations.length; i++) {
|
||||
// Check timeout
|
||||
if (Date.now() - this.startTime > this.options.timeout) {
|
||||
throw new TransactionTimeoutError(this.options.timeout, i)
|
||||
}
|
||||
|
||||
const operation = this.operations[i]
|
||||
|
||||
try {
|
||||
if (this.options.logging) {
|
||||
prodLog.info(`[Transaction] Executing operation ${i}: ${operation.name || 'unnamed'}`)
|
||||
}
|
||||
|
||||
// Execute operation
|
||||
const rollbackAction = await operation.execute()
|
||||
|
||||
// Record rollback action (if provided)
|
||||
if (rollbackAction) {
|
||||
this.rollbackActions.push(rollbackAction)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Operation failed - rollback and re-throw
|
||||
const executionError = new TransactionExecutionError(
|
||||
`Operation ${i} failed: ${(error as Error).message}`,
|
||||
i,
|
||||
operation.name,
|
||||
error as Error
|
||||
)
|
||||
|
||||
await this.rollback(executionError)
|
||||
throw executionError
|
||||
}
|
||||
}
|
||||
|
||||
// All operations succeeded - commit
|
||||
this.commit()
|
||||
|
||||
} catch (error) {
|
||||
// Error already handled in rollback
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the transaction
|
||||
*/
|
||||
private commit(): void {
|
||||
this.state = 'committed'
|
||||
this.endTime = Date.now()
|
||||
|
||||
if (this.options.logging) {
|
||||
const duration = this.endTime - (this.startTime || this.endTime)
|
||||
prodLog.info(`[Transaction] Committed successfully in ${duration}ms`)
|
||||
}
|
||||
|
||||
// Clear rollback actions - no longer needed
|
||||
this.rollbackActions = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback all executed operations in reverse order
|
||||
*/
|
||||
private async rollback(originalError: Error): Promise<void> {
|
||||
this.state = 'rolling_back'
|
||||
|
||||
if (this.options.logging) {
|
||||
prodLog.info(`[Transaction] Rolling back ${this.rollbackActions.length} operations`)
|
||||
}
|
||||
|
||||
const rollbackErrors: Error[] = []
|
||||
|
||||
// Execute rollback actions in REVERSE order
|
||||
for (let i = this.rollbackActions.length - 1; i >= 0; i--) {
|
||||
const action = this.rollbackActions[i]
|
||||
|
||||
// Retry rollback with exponential backoff
|
||||
let attempts = 0
|
||||
let success = false
|
||||
|
||||
while (attempts < this.options.maxRollbackRetries && !success) {
|
||||
try {
|
||||
await action()
|
||||
success = true
|
||||
|
||||
if (this.options.logging) {
|
||||
prodLog.info(`[Transaction] Rolled back operation ${i}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
attempts++
|
||||
|
||||
if (attempts >= this.options.maxRollbackRetries) {
|
||||
// Max retries exceeded - log error and continue
|
||||
const rollbackError = error as Error
|
||||
rollbackErrors.push(rollbackError)
|
||||
|
||||
prodLog.error(
|
||||
`[Transaction] Rollback failed for operation ${i} after ${attempts} attempts: ${rollbackError.message}`
|
||||
)
|
||||
} else {
|
||||
// Retry with exponential backoff
|
||||
const delayMs = Math.pow(2, attempts) * 100
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.state = 'rolled_back'
|
||||
this.endTime = Date.now()
|
||||
|
||||
if (this.options.logging) {
|
||||
const duration = this.endTime - (this.startTime || this.endTime)
|
||||
prodLog.info(`[Transaction] Rolled back in ${duration}ms`)
|
||||
}
|
||||
|
||||
// If rollback encountered errors, wrap them with original error
|
||||
if (rollbackErrors.length > 0) {
|
||||
throw new TransactionRollbackError(
|
||||
`Transaction rollback encountered ${rollbackErrors.length} errors during cleanup`,
|
||||
originalError,
|
||||
rollbackErrors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction execution time in milliseconds
|
||||
*/
|
||||
getExecutionTimeMs(): number | undefined {
|
||||
if (this.startTime && this.endTime) {
|
||||
return this.endTime - this.startTime
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue