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:
David Snelling 2025-11-14 10:26:23 -08:00
parent 52e961760d
commit e40fee39d8
21 changed files with 5657 additions and 182 deletions

View 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
}
}

View file

@ -0,0 +1,189 @@
/**
* Transaction Manager
*
* Manages transaction execution across the system.
* Provides high-level API for executing atomic operations.
*
* Usage:
* ```typescript
* const txManager = new TransactionManager()
*
* const result = await txManager.executeTransaction(async (tx) => {
* tx.addOperation(operation1)
* tx.addOperation(operation2)
* return someValue
* })
* ```
*/
import { Transaction } from './Transaction.js'
import {
TransactionFunction,
TransactionResult,
TransactionOptions
} from './types.js'
import { TransactionError } from './errors.js'
import { prodLog } from '../utils/logger.js'
/**
* Transaction Manager Statistics
*/
export interface TransactionStats {
totalTransactions: number
successfulTransactions: number
failedTransactions: number
rolledBackTransactions: number
averageExecutionTimeMs: number
averageOperationsPerTransaction: number
}
/**
* Transaction Manager
*/
export class TransactionManager {
private stats: TransactionStats = {
totalTransactions: 0,
successfulTransactions: 0,
failedTransactions: 0,
rolledBackTransactions: 0,
averageExecutionTimeMs: 0,
averageOperationsPerTransaction: 0
}
private totalExecutionTime = 0
private totalOperations = 0
/**
* Execute a function within a transaction
* All operations succeed atomically or all rollback
*
* @param fn Function that builds and executes transaction
* @param options Transaction execution options
* @returns Result from user function
* @throws TransactionError if transaction fails
*/
async executeTransaction<T>(
fn: TransactionFunction<T>,
options?: TransactionOptions
): Promise<T> {
const transaction = new Transaction(options)
this.stats.totalTransactions++
try {
// Execute user function (builds operations list)
const result = await fn(transaction)
// Execute all operations atomically
await transaction.execute()
// Update statistics
this.stats.successfulTransactions++
this.updateStats(transaction)
return result
} catch (error) {
// Transaction failed and rolled back
this.stats.failedTransactions++
if (transaction.getState() === 'rolled_back') {
this.stats.rolledBackTransactions++
}
this.updateStats(transaction)
// Re-throw with context
if (error instanceof TransactionError) {
throw error
} else {
throw new TransactionError(
`Transaction failed: ${(error as Error).message}`,
{ cause: error }
)
}
}
}
/**
* Execute a transaction and return detailed result
*/
async executeTransactionWithResult<T>(
fn: TransactionFunction<T>,
options?: TransactionOptions
): Promise<TransactionResult<T>> {
const startTime = Date.now()
const transaction = new Transaction(options)
try {
const value = await fn(transaction)
await transaction.execute()
const executionTimeMs = Date.now() - startTime
return {
value,
operationCount: transaction.getOperationCount(),
executionTimeMs
}
} catch (error) {
// Transaction failed
throw error
}
}
/**
* Get transaction statistics
*/
getStats(): Readonly<TransactionStats> {
return { ...this.stats }
}
/**
* Reset transaction statistics
*/
resetStats(): void {
this.stats = {
totalTransactions: 0,
successfulTransactions: 0,
failedTransactions: 0,
rolledBackTransactions: 0,
averageExecutionTimeMs: 0,
averageOperationsPerTransaction: 0
}
this.totalExecutionTime = 0
this.totalOperations = 0
}
/**
* Update running statistics
*/
private updateStats(transaction: Transaction): void {
const executionTime = transaction.getExecutionTimeMs()
const operationCount = transaction.getOperationCount()
if (executionTime !== undefined) {
this.totalExecutionTime += executionTime
this.stats.averageExecutionTimeMs =
this.totalExecutionTime / this.stats.totalTransactions
}
this.totalOperations += operationCount
this.stats.averageOperationsPerTransaction =
this.totalOperations / this.stats.totalTransactions
}
/**
* Log current statistics
*/
logStats(): void {
prodLog.info('[TransactionManager] Statistics:')
prodLog.info(` Total: ${this.stats.totalTransactions}`)
prodLog.info(` Successful: ${this.stats.successfulTransactions}`)
prodLog.info(` Failed: ${this.stats.failedTransactions}`)
prodLog.info(` Rolled back: ${this.stats.rolledBackTransactions}`)
prodLog.info(` Avg execution time: ${this.stats.averageExecutionTimeMs.toFixed(2)}ms`)
prodLog.info(` Avg operations/tx: ${this.stats.averageOperationsPerTransaction.toFixed(2)}`)
}
}

88
src/transaction/errors.ts Normal file
View file

@ -0,0 +1,88 @@
/**
* Transaction System Errors
*
* Provides detailed error information for transaction failures.
*/
/**
* Base class for all transaction errors
*/
export class TransactionError extends Error {
constructor(
message: string,
public readonly context?: Record<string, any>
) {
super(message)
this.name = 'TransactionError'
Error.captureStackTrace(this, this.constructor)
}
}
/**
* Error during transaction execution
*/
export class TransactionExecutionError extends TransactionError {
constructor(
message: string,
public readonly operationIndex: number,
public readonly operationName: string | undefined,
public readonly cause: Error
) {
super(message, {
operationIndex,
operationName,
cause: cause.message
})
this.name = 'TransactionExecutionError'
}
}
/**
* Error during transaction rollback
*/
export class TransactionRollbackError extends TransactionError {
constructor(
message: string,
public readonly originalError: Error,
public readonly rollbackErrors: Error[]
) {
super(message, {
originalError: originalError.message,
rollbackErrorCount: rollbackErrors.length,
rollbackErrors: rollbackErrors.map(e => e.message)
})
this.name = 'TransactionRollbackError'
}
}
/**
* Error for invalid transaction state
*/
export class InvalidTransactionStateError extends TransactionError {
constructor(
currentState: string,
attemptedAction: string
) {
super(
`Cannot ${attemptedAction}: transaction is in state '${currentState}'`,
{ currentState, attemptedAction }
)
this.name = 'InvalidTransactionStateError'
}
}
/**
* Error for transaction timeout
*/
export class TransactionTimeoutError extends TransactionError {
constructor(
timeoutMs: number,
operationIndex: number
) {
super(
`Transaction timed out after ${timeoutMs}ms at operation ${operationIndex}`,
{ timeoutMs, operationIndex }
)
this.name = 'TransactionTimeoutError'
}
}

32
src/transaction/index.ts Normal file
View file

@ -0,0 +1,32 @@
/**
* Transaction System
*
* Provides atomicity for Brainy operations.
* All operations succeed or all rollback - no partial failures.
*
* @module transaction
*/
export * from './types.js'
export * from './errors.js'
export { Transaction } from './Transaction.js'
export { TransactionManager, type TransactionStats } from './TransactionManager.js'
// Re-export for convenience
export type {
Operation,
RollbackAction,
TransactionState,
TransactionContext,
TransactionFunction,
TransactionResult,
TransactionOptions
} from './types.js'
export {
TransactionError,
TransactionExecutionError,
TransactionRollbackError,
InvalidTransactionStateError,
TransactionTimeoutError
} from './errors.js'

View file

@ -0,0 +1,361 @@
/**
* Index Operations with Rollback Support
*
* Provides transactional operations for all indexes:
* - TypeAwareHNSWIndex (per-type vector indexes)
* - HNSWIndex (legacy/fallback vector index)
* - MetadataIndexManager (roaring bitmap filtering)
* - GraphAdjacencyIndex (LSM-tree graph storage)
*
* Each operation can be executed and rolled back atomically.
*/
import type { HNSWIndex } from '../../hnsw/hnswIndex.js'
import type { TypeAwareHNSWIndex } from '../../hnsw/typeAwareHNSWIndex.js'
import type { MetadataIndexManager } from '../../utils/metadataIndex.js'
import type { GraphAdjacencyIndex } from '../../graph/graphAdjacencyIndex.js'
import type { NounType } from '../../types/graphTypes.js'
import type { GraphVerb } from '../../coreTypes.js'
import type { Operation, RollbackAction } from '../types.js'
/**
* Add to HNSW index with rollback support
*
* Rollback strategy:
* - Remove item from index
*
* Note: Works with both HNSWIndex and TypeAwareHNSWIndex
*/
export class AddToHNSWOperation implements Operation {
readonly name = 'AddToHNSW'
constructor(
private readonly index: HNSWIndex,
private readonly id: string,
private readonly vector: number[]
) {}
async execute(): Promise<RollbackAction> {
// Check if item already exists (for rollback decision)
const existed = await this.itemExists(this.id)
// Add to index
await this.index.addItem({ id: this.id, vector: this.vector })
// Return rollback action
return async () => {
if (!existed) {
// Remove newly added item
await this.index.removeItem(this.id)
}
// If item existed before, we don't rollback (update is OK)
// This prevents index corruption from removing pre-existing items
}
}
/**
* Check if item exists in index
*/
private async itemExists(id: string): Promise<boolean> {
try {
// Try to get item - if exists, no error
await (this.index as any).getItem?.(id)
return true
} catch {
return false
}
}
}
/**
* Add to TypeAwareHNSW index with rollback support
*
* Rollback strategy:
* - Remove item from type-specific index
*/
export class AddToTypeAwareHNSWOperation implements Operation {
readonly name = 'AddToTypeAwareHNSW'
constructor(
private readonly index: TypeAwareHNSWIndex,
private readonly id: string,
private readonly vector: number[],
private readonly nounType: NounType
) {}
async execute(): Promise<RollbackAction> {
// Check if item already exists
const existed = await this.itemExists(this.id, this.nounType)
// Add to type-specific index
await this.index.addItem({ id: this.id, vector: this.vector }, this.nounType)
// Return rollback action
return async () => {
if (!existed) {
// Remove newly added item from type-specific index
await this.index.removeItem(this.id, this.nounType)
}
}
}
/**
* Check if item exists in type-specific index
*/
private async itemExists(id: string, type: NounType): Promise<boolean> {
try {
const index = (this.index as any).getIndexForType?.(type)
if (index) {
await index.getItem?.(id)
return true
}
return false
} catch {
return false
}
}
}
/**
* Remove from HNSW index with rollback support
*
* Rollback strategy:
* - Re-add item to index with original vector
*
* Note: Requires storing the vector for rollback
*/
export class RemoveFromHNSWOperation implements Operation {
readonly name = 'RemoveFromHNSW'
constructor(
private readonly index: HNSWIndex,
private readonly id: string,
private readonly vector: number[] // Required for rollback
) {}
async execute(): Promise<RollbackAction> {
// Remove from index
await this.index.removeItem(this.id)
// Return rollback action
return async () => {
// Re-add item with original vector
await this.index.addItem({ id: this.id, vector: this.vector })
}
}
}
/**
* Remove from TypeAwareHNSW index with rollback support
*
* Rollback strategy:
* - Re-add item to type-specific index with original vector
*/
export class RemoveFromTypeAwareHNSWOperation implements Operation {
readonly name = 'RemoveFromTypeAwareHNSW'
constructor(
private readonly index: TypeAwareHNSWIndex,
private readonly id: string,
private readonly vector: number[], // Required for rollback
private readonly nounType: NounType
) {}
async execute(): Promise<RollbackAction> {
// Remove from type-specific index
await this.index.removeItem(this.id, this.nounType)
// Return rollback action
return async () => {
// Re-add item with original vector to type-specific index
await this.index.addItem({ id: this.id, vector: this.vector }, this.nounType)
}
}
}
/**
* Add to metadata index with rollback support
*
* Rollback strategy:
* - Remove item from index
*/
export class AddToMetadataIndexOperation implements Operation {
readonly name = 'AddToMetadataIndex'
constructor(
private readonly index: MetadataIndexManager,
private readonly id: string,
private readonly entity: any // Entity or metadata structure
) {}
async execute(): Promise<RollbackAction> {
// Add to metadata index (skipFlush=true for transaction atomicity)
await this.index.addToIndex(this.id, this.entity, true)
// Return rollback action
return async () => {
// Remove from metadata index
await this.index.removeFromIndex(this.id, this.entity)
}
}
}
/**
* Remove from metadata index with rollback support
*
* Rollback strategy:
* - Re-add item to index with original metadata
*/
export class RemoveFromMetadataIndexOperation implements Operation {
readonly name = 'RemoveFromMetadataIndex'
constructor(
private readonly index: MetadataIndexManager,
private readonly id: string,
private readonly entity: any // Required for rollback
) {}
async execute(): Promise<RollbackAction> {
// Remove from metadata index
await this.index.removeFromIndex(this.id, this.entity)
// Return rollback action
return async () => {
// Re-add with original metadata (skipFlush=true)
await this.index.addToIndex(this.id, this.entity, true)
}
}
}
/**
* Add verb to graph index with rollback support
*
* Rollback strategy:
* - Remove verb from graph index
*/
export class AddToGraphIndexOperation implements Operation {
readonly name = 'AddToGraphIndex'
constructor(
private readonly index: GraphAdjacencyIndex,
private readonly verb: GraphVerb
) {}
async execute(): Promise<RollbackAction> {
// Add verb to graph index
await this.index.addVerb(this.verb)
// Return rollback action
return async () => {
// Remove verb from graph index
await this.index.removeVerb(this.verb.id)
}
}
}
/**
* Remove verb from graph index with rollback support
*
* Rollback strategy:
* - Re-add verb to graph index
*/
export class RemoveFromGraphIndexOperation implements Operation {
readonly name = 'RemoveFromGraphIndex'
constructor(
private readonly index: GraphAdjacencyIndex,
private readonly verb: GraphVerb // Required for rollback
) {}
async execute(): Promise<RollbackAction> {
// Remove verb from graph index
await this.index.removeVerb(this.verb.id)
// Return rollback action
return async () => {
// Re-add verb with original data
await this.index.addVerb(this.verb)
}
}
}
/**
* Batch operation: Add multiple items to HNSW index
*
* Useful for bulk imports with transaction support.
* Rolls back all items if any fail.
*/
export class BatchAddToHNSWOperation implements Operation {
readonly name = 'BatchAddToHNSW'
private operations: AddToHNSWOperation[]
constructor(
index: HNSWIndex,
items: Array<{ id: string; vector: number[] }>
) {
this.operations = items.map(
item => new AddToHNSWOperation(index, item.id, item.vector)
)
}
async execute(): Promise<RollbackAction> {
const rollbackActions: RollbackAction[] = []
// Execute all operations
for (const op of this.operations) {
const rollback = await op.execute()
if (rollback) {
rollbackActions.push(rollback)
}
}
// Return combined rollback action
return async () => {
// Execute all rollbacks in reverse order
for (let i = rollbackActions.length - 1; i >= 0; i--) {
await rollbackActions[i]()
}
}
}
}
/**
* Batch operation: Add multiple entities to metadata index
*
* Useful for bulk imports with transaction support.
*/
export class BatchAddToMetadataIndexOperation implements Operation {
readonly name = 'BatchAddToMetadataIndex'
private operations: AddToMetadataIndexOperation[]
constructor(
index: MetadataIndexManager,
items: Array<{ id: string; entity: any }>
) {
this.operations = items.map(
item => new AddToMetadataIndexOperation(index, item.id, item.entity)
)
}
async execute(): Promise<RollbackAction> {
const rollbackActions: RollbackAction[] = []
// Execute all operations
for (const op of this.operations) {
const rollback = await op.execute()
if (rollback) {
rollbackActions.push(rollback)
}
}
// Return combined rollback action
return async () => {
// Execute all rollbacks in reverse order
for (let i = rollbackActions.length - 1; i >= 0; i--) {
await rollbackActions[i]()
}
}
}
}

View file

@ -0,0 +1,301 @@
/**
* Storage Operations with Rollback Support
*
* Provides transactional operations for all storage adapters.
* Each operation can be executed and rolled back atomically.
*
* Supports:
* - All 8 storage adapters (FileSystem, S3, Memory, OPFS, GCS, Azure, R2, TypeAware)
* - Nouns (entities) and Verbs (relationships)
* - Metadata and vector data
* - COW (Copy-on-Write) storage
*/
import type { StorageAdapter, HNSWNoun, HNSWVerb, NounMetadata, VerbMetadata } from '../../coreTypes.js'
import type { Operation, RollbackAction } from '../types.js'
/**
* Save noun metadata with rollback support
*
* Rollback strategy:
* - If metadata existed: Restore previous metadata
* - If metadata was new: Delete metadata
*/
export class SaveNounMetadataOperation implements Operation {
readonly name = 'SaveNounMetadata'
constructor(
private readonly storage: StorageAdapter,
private readonly id: string,
private readonly metadata: NounMetadata
) {}
async execute(): Promise<RollbackAction> {
// Get existing metadata (for rollback)
const previousMetadata = await this.storage.getNounMetadata(this.id)
// Save new metadata
await this.storage.saveNounMetadata(this.id, this.metadata)
// Return rollback action
return async () => {
if (previousMetadata) {
// Restore previous metadata
await this.storage.saveNounMetadata(this.id, previousMetadata)
} else {
// Delete newly created metadata
await this.storage.deleteNounMetadata(this.id)
}
}
}
}
/**
* Save noun (vector data) with rollback support
*
* Rollback strategy:
* - If noun existed: Restore previous noun
* - If noun was new: Delete noun (if deleteNoun exists on adapter)
*
* Note: Not all adapters implement deleteNoun - this is acceptable
* because orphaned vector data without metadata is invisible to queries
*/
export class SaveNounOperation implements Operation {
readonly name = 'SaveNoun'
constructor(
private readonly storage: StorageAdapter,
private readonly noun: HNSWNoun
) {}
async execute(): Promise<RollbackAction> {
// Get existing noun (for rollback)
const previousNoun = await this.storage.getNoun(this.noun.id)
// Save new noun
await this.storage.saveNoun(this.noun)
// Return rollback action
return async () => {
if (previousNoun) {
// Restore previous noun (extract just vector data)
const nounData: HNSWNoun = {
id: previousNoun.id,
vector: previousNoun.vector,
connections: previousNoun.connections || new Map(),
level: previousNoun.level || 0
}
await this.storage.saveNoun(nounData)
} else {
// Delete newly created noun (if adapter supports it)
// Note: Not all adapters implement deleteNoun
// This is acceptable - metadata deletion makes entity invisible
if ('deleteNoun' in this.storage && typeof (this.storage as any).deleteNoun === 'function') {
await (this.storage as any).deleteNoun(this.noun.id)
}
}
}
}
}
/**
* Delete noun metadata with rollback support
*
* Rollback strategy:
* - Restore deleted metadata
*/
export class DeleteNounMetadataOperation implements Operation {
readonly name = 'DeleteNounMetadata'
constructor(
private readonly storage: StorageAdapter,
private readonly id: string
) {}
async execute(): Promise<RollbackAction> {
// Get metadata before deletion (for rollback)
const previousMetadata = await this.storage.getNounMetadata(this.id)
if (!previousMetadata) {
// Nothing to delete - no rollback needed
return async () => {}
}
// Delete metadata
await this.storage.deleteNounMetadata(this.id)
// Return rollback action
return async () => {
// Restore deleted metadata
await this.storage.saveNounMetadata(this.id, previousMetadata)
}
}
}
/**
* Save verb metadata with rollback support
*
* Rollback strategy:
* - If metadata existed: Restore previous metadata
* - If metadata was new: Delete metadata
*/
export class SaveVerbMetadataOperation implements Operation {
readonly name = 'SaveVerbMetadata'
constructor(
private readonly storage: StorageAdapter,
private readonly id: string,
private readonly metadata: VerbMetadata
) {}
async execute(): Promise<RollbackAction> {
// Get existing metadata (for rollback)
const previousMetadata = await this.storage.getVerbMetadata(this.id)
// Save new metadata
await this.storage.saveVerbMetadata(this.id, this.metadata)
// Return rollback action
return async () => {
if (previousMetadata) {
// Restore previous metadata
await this.storage.saveVerbMetadata(this.id, previousMetadata)
} else {
// Delete newly created verb (metadata + vector)
// Note: StorageAdapter has deleteVerb but not deleteVerbMetadata
await this.storage.deleteVerb(this.id)
}
}
}
}
/**
* Save verb (vector data) with rollback support
*
* Rollback strategy:
* - If verb existed: Restore previous verb
* - If verb was new: Delete verb (if deleteVerb exists on adapter)
*/
export class SaveVerbOperation implements Operation {
readonly name = 'SaveVerb'
constructor(
private readonly storage: StorageAdapter,
private readonly verb: HNSWVerb
) {}
async execute(): Promise<RollbackAction> {
// Get existing verb (for rollback)
const previousVerb = await this.storage.getVerb(this.verb.id)
// Save new verb
await this.storage.saveVerb(this.verb)
// Return rollback action
return async () => {
if (previousVerb) {
// Restore previous verb (extract just vector data)
const verbData: HNSWVerb = {
id: previousVerb.id,
sourceId: previousVerb.sourceId,
targetId: previousVerb.targetId,
verb: previousVerb.verb,
vector: previousVerb.vector,
connections: previousVerb.connections || new Map()
}
await this.storage.saveVerb(verbData)
} else {
// Delete newly created verb (if adapter supports it)
if ('deleteVerb' in this.storage && typeof (this.storage as any).deleteVerb === 'function') {
await (this.storage as any).deleteVerb(this.verb.id)
}
}
}
}
}
/**
* Delete verb metadata with rollback support
*
* Rollback strategy:
* - Restore deleted metadata
*/
export class DeleteVerbMetadataOperation implements Operation {
readonly name = 'DeleteVerbMetadata'
constructor(
private readonly storage: StorageAdapter,
private readonly id: string
) {}
async execute(): Promise<RollbackAction> {
// Get metadata before deletion (for rollback)
const previousMetadata = await this.storage.getVerbMetadata(this.id)
if (!previousMetadata) {
// Nothing to delete - no rollback needed
return async () => {}
}
// Delete verb (metadata + vector)
// Note: StorageAdapter has deleteVerb but not deleteVerbMetadata
await this.storage.deleteVerb(this.id)
// Return rollback action
return async () => {
// Restore deleted metadata
await this.storage.saveVerbMetadata(this.id, previousMetadata)
}
}
}
/**
* Update noun metadata with rollback support
*
* Rollback strategy:
* - Restore previous metadata
*
* Note: This is a convenience operation that wraps SaveNounMetadataOperation
* with explicit "update" semantics
*/
export class UpdateNounMetadataOperation implements Operation {
readonly name = 'UpdateNounMetadata'
private saveOperation: SaveNounMetadataOperation
constructor(
storage: StorageAdapter,
id: string,
metadata: NounMetadata
) {
this.saveOperation = new SaveNounMetadataOperation(storage, id, metadata)
}
async execute(): Promise<RollbackAction> {
return await this.saveOperation.execute()
}
}
/**
* Update verb metadata with rollback support
*
* Rollback strategy:
* - Restore previous metadata
*/
export class UpdateVerbMetadataOperation implements Operation {
readonly name = 'UpdateVerbMetadata'
private saveOperation: SaveVerbMetadataOperation
constructor(
storage: StorageAdapter,
id: string,
metadata: VerbMetadata
) {
this.saveOperation = new SaveVerbMetadataOperation(storage, id, metadata)
}
async execute(): Promise<RollbackAction> {
return await this.saveOperation.execute()
}
}

View file

@ -0,0 +1,34 @@
/**
* Transaction Operations
*
* Re-exports all transactional operations for storage and indexes.
* These operations provide atomicity with rollback support.
*
* @module transaction/operations
*/
// Storage Operations
export {
SaveNounMetadataOperation,
SaveNounOperation,
DeleteNounMetadataOperation,
SaveVerbMetadataOperation,
SaveVerbOperation,
DeleteVerbMetadataOperation,
UpdateNounMetadataOperation,
UpdateVerbMetadataOperation
} from './StorageOperations.js'
// Index Operations
export {
AddToHNSWOperation,
AddToTypeAwareHNSWOperation,
RemoveFromHNSWOperation,
RemoveFromTypeAwareHNSWOperation,
AddToMetadataIndexOperation,
RemoveFromMetadataIndexOperation,
AddToGraphIndexOperation,
RemoveFromGraphIndexOperation,
BatchAddToHNSWOperation,
BatchAddToMetadataIndexOperation
} from './IndexOperations.js'

103
src/transaction/types.ts Normal file
View file

@ -0,0 +1,103 @@
/**
* Transaction System Types
*
* Provides atomicity for Brainy operations - all succeed or all rollback.
* Prevents partial failures that leave system in inconsistent state.
*/
/**
* Transaction state
*/
export type TransactionState =
| 'pending' // Created but not executed
| 'executing' // Currently executing operations
| 'committed' // Successfully committed
| 'rolling_back' // Rolling back due to failure
| 'rolled_back' // Successfully rolled back
/**
* Rollback action - undoes an operation
* Must be idempotent (safe to call multiple times)
*/
export type RollbackAction = () => Promise<void>
/**
* Operation that can be executed and rolled back
*/
export interface Operation {
/**
* Execute the operation
* @returns Rollback action to undo this operation (or undefined if no rollback needed)
*/
execute(): Promise<RollbackAction | undefined>
/**
* Optional: Name of operation for debugging
*/
readonly name?: string
}
/**
* Transaction context passed to user functions
*/
export interface TransactionContext {
/**
* Add an operation to the transaction
*/
addOperation(operation: Operation): void
/**
* Get current transaction state
*/
getState(): TransactionState
/**
* Get number of operations in transaction
*/
getOperationCount(): number
}
/**
* Function that builds a transaction
*/
export type TransactionFunction<T> = (ctx: TransactionContext) => Promise<T>
/**
* Transaction execution result
*/
export interface TransactionResult<T> {
/**
* Result value from user function
*/
value: T
/**
* Number of operations executed
*/
operationCount: number
/**
* Execution time in milliseconds
*/
executionTimeMs: number
}
/**
* Transaction execution options
*/
export interface TransactionOptions {
/**
* Timeout in milliseconds (default: 30000 = 30 seconds)
*/
timeout?: number
/**
* Whether to log transaction execution (default: false)
*/
logging?: boolean
/**
* Maximum number of rollback retry attempts (default: 3)
*/
maxRollbackRetries?: number
}