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
387
src/brainy.ts
387
src/brainy.ts
|
|
@ -32,6 +32,24 @@ import { BlobStorage } from './storage/cow/BlobStorage.js'
|
|||
import { NULL_HASH } from './storage/cow/constants.js'
|
||||
import { createPipeline } from './streaming/pipeline.js'
|
||||
import { configureLogger, LogLevel } from './utils/logger.js'
|
||||
import { TransactionManager } from './transaction/TransactionManager.js'
|
||||
import {
|
||||
SaveNounMetadataOperation,
|
||||
SaveNounOperation,
|
||||
AddToTypeAwareHNSWOperation,
|
||||
AddToHNSWOperation,
|
||||
AddToMetadataIndexOperation,
|
||||
SaveVerbMetadataOperation,
|
||||
SaveVerbOperation,
|
||||
AddToGraphIndexOperation,
|
||||
RemoveFromHNSWOperation,
|
||||
RemoveFromTypeAwareHNSWOperation,
|
||||
RemoveFromMetadataIndexOperation,
|
||||
RemoveFromGraphIndexOperation,
|
||||
UpdateNounMetadataOperation,
|
||||
DeleteNounMetadataOperation,
|
||||
DeleteVerbMetadataOperation
|
||||
} from './transaction/operations/index.js'
|
||||
import {
|
||||
DistributedCoordinator,
|
||||
ShardManager,
|
||||
|
|
@ -74,6 +92,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
private storage!: BaseStorage
|
||||
private metadataIndex!: MetadataIndexManager
|
||||
private graphIndex!: GraphAdjacencyIndex
|
||||
private transactionManager: TransactionManager
|
||||
private embedder: EmbeddingFunction
|
||||
private distance: DistanceFunction
|
||||
private augmentationRegistry: AugmentationRegistry
|
||||
|
|
@ -119,6 +138,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
this.distance = cosineDistance
|
||||
this.embedder = this.setupEmbedder()
|
||||
this.augmentationRegistry = this.setupAugmentations()
|
||||
this.transactionManager = new TransactionManager()
|
||||
|
||||
// Setup distributed components if enabled
|
||||
if (this.config.distributed?.enabled) {
|
||||
|
|
@ -432,26 +452,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
...(params.createdBy && { createdBy: params.createdBy })
|
||||
}
|
||||
|
||||
// v5.0.1: Save metadata FIRST so TypeAwareStorage can cache the type
|
||||
// This prevents the race condition where saveNoun() defaults to 'thing'
|
||||
await this.storage.saveNounMetadata(id, storageMetadata)
|
||||
|
||||
// Then save vector
|
||||
await this.storage.saveNoun({
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0
|
||||
})
|
||||
|
||||
// v5.4.0: Add to HNSW index AFTER entity is saved (fixes race condition)
|
||||
// CRITICAL: Entity must exist in storage before HNSW tries to persist
|
||||
if (this.index instanceof TypeAwareHNSWIndex) {
|
||||
await this.index.addItem({ id, vector }, params.type as any)
|
||||
} else {
|
||||
await this.index.addItem({ id, vector })
|
||||
}
|
||||
|
||||
// v4.8.0: Build entity structure for indexing (NEW - with top-level fields)
|
||||
const entityForIndexing = {
|
||||
id,
|
||||
|
|
@ -470,8 +470,45 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
metadata: params.metadata || {}
|
||||
}
|
||||
|
||||
// Pass full entity structure to metadata index
|
||||
await this.metadataIndex.addToIndex(id, entityForIndexing)
|
||||
// v5.8.0: Execute atomically with transaction system
|
||||
// All operations succeed or all rollback - prevents partial failures
|
||||
await this.transactionManager.executeTransaction(async (tx) => {
|
||||
// Operation 1: Save metadata FIRST (v5.0.1 - TypeAwareStorage caching)
|
||||
tx.addOperation(
|
||||
new SaveNounMetadataOperation(this.storage, id, storageMetadata)
|
||||
)
|
||||
|
||||
// Operation 2: Save vector data
|
||||
tx.addOperation(
|
||||
new SaveNounOperation(this.storage, {
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0
|
||||
})
|
||||
)
|
||||
|
||||
// Operation 3: Add to HNSW index (v5.4.0 - after entity saved)
|
||||
if (this.index instanceof TypeAwareHNSWIndex) {
|
||||
tx.addOperation(
|
||||
new AddToTypeAwareHNSWOperation(
|
||||
this.index,
|
||||
id,
|
||||
vector,
|
||||
params.type as any
|
||||
)
|
||||
)
|
||||
} else {
|
||||
tx.addOperation(
|
||||
new AddToHNSWOperation(this.index, id, vector)
|
||||
)
|
||||
}
|
||||
|
||||
// Operation 4: Add to metadata index
|
||||
tx.addOperation(
|
||||
new AddToMetadataIndexOperation(this.metadataIndex, id, entityForIndexing)
|
||||
)
|
||||
})
|
||||
|
||||
return id
|
||||
})
|
||||
|
|
@ -683,34 +720,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
...(params.weight === undefined && existing.weight !== undefined && { weight: existing.weight })
|
||||
}
|
||||
|
||||
// v4.0.0: Save metadata FIRST (v5.1.0 fix: updates type cache for TypeAwareStorage)
|
||||
// v5.1.0: saveNounMetadata must be called before saveNoun so that the type cache
|
||||
// is updated before determining the shard path. Otherwise type changes cause
|
||||
// entities to be saved in the wrong shard and become unfindable.
|
||||
await this.storage.saveNounMetadata(params.id, updatedMetadata)
|
||||
|
||||
// Then save vector (will use updated type cache)
|
||||
await this.storage.saveNoun({
|
||||
id: params.id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0
|
||||
})
|
||||
|
||||
// v5.4.0: Update HNSW index AFTER entity is saved (fixes race condition)
|
||||
// CRITICAL: Entity must be fully updated in storage before HNSW tries to persist
|
||||
if (needsReindexing) {
|
||||
// Update in index (remove and re-add since no update method)
|
||||
// Phase 2: pass type for TypeAwareHNSWIndex
|
||||
if (this.index instanceof TypeAwareHNSWIndex) {
|
||||
await this.index.removeItem(params.id, existing.type as any)
|
||||
await this.index.addItem({ id: params.id, vector }, newType as any) // v5.1.0: use new type
|
||||
} else {
|
||||
await this.index.removeItem(params.id)
|
||||
await this.index.addItem({ id: params.id, vector })
|
||||
}
|
||||
}
|
||||
|
||||
// v4.8.0: Build entity structure for metadata index (with top-level fields)
|
||||
const entityForIndexing = {
|
||||
id: params.id,
|
||||
|
|
@ -729,9 +738,60 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
metadata: newMetadata
|
||||
}
|
||||
|
||||
// Update metadata index - remove old entry and add new one with v4.8.0 structure
|
||||
await this.metadataIndex.removeFromIndex(params.id, existing.metadata)
|
||||
await this.metadataIndex.addToIndex(params.id, entityForIndexing)
|
||||
// v5.8.0: Execute atomically with transaction system
|
||||
await this.transactionManager.executeTransaction(async (tx) => {
|
||||
// Operation 1: Update metadata FIRST (v5.1.0 - updates type cache)
|
||||
tx.addOperation(
|
||||
new UpdateNounMetadataOperation(this.storage, params.id, updatedMetadata)
|
||||
)
|
||||
|
||||
// Operation 2: Update vector data (will use updated type cache)
|
||||
tx.addOperation(
|
||||
new SaveNounOperation(this.storage, {
|
||||
id: params.id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0
|
||||
})
|
||||
)
|
||||
|
||||
// Operation 3-4: Update HNSW index (remove and re-add if reindexing needed)
|
||||
if (needsReindexing) {
|
||||
if (this.index instanceof TypeAwareHNSWIndex) {
|
||||
tx.addOperation(
|
||||
new RemoveFromTypeAwareHNSWOperation(
|
||||
this.index,
|
||||
params.id,
|
||||
existing.vector,
|
||||
existing.type as any
|
||||
)
|
||||
)
|
||||
tx.addOperation(
|
||||
new AddToTypeAwareHNSWOperation(
|
||||
this.index,
|
||||
params.id,
|
||||
vector,
|
||||
newType as any
|
||||
)
|
||||
)
|
||||
} else {
|
||||
tx.addOperation(
|
||||
new RemoveFromHNSWOperation(this.index, params.id, existing.vector)
|
||||
)
|
||||
tx.addOperation(
|
||||
new AddToHNSWOperation(this.index, params.id, vector)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Operation 5-6: Update metadata index (remove old, add new)
|
||||
tx.addOperation(
|
||||
new RemoveFromMetadataIndexOperation(this.metadataIndex, params.id, existing.metadata)
|
||||
)
|
||||
tx.addOperation(
|
||||
new AddToMetadataIndexOperation(this.metadataIndex, params.id, entityForIndexing)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -747,49 +807,57 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
await this.ensureInitialized()
|
||||
|
||||
return this.augmentationRegistry.execute('delete', { id }, async () => {
|
||||
// Remove from vector index (Phase 2: get type for TypeAwareHNSWIndex)
|
||||
if (this.index instanceof TypeAwareHNSWIndex) {
|
||||
// Get entity metadata to determine type
|
||||
const metadata = await this.storage.getNounMetadata(id)
|
||||
if (metadata && metadata.noun) {
|
||||
await this.index.removeItem(id, metadata.noun as any)
|
||||
}
|
||||
} else {
|
||||
await this.index.removeItem(id)
|
||||
}
|
||||
|
||||
// Remove from metadata index
|
||||
await this.metadataIndex.removeFromIndex(id)
|
||||
|
||||
// Delete from storage
|
||||
await this.storage.deleteNoun(id)
|
||||
|
||||
// Delete metadata (if it exists as separate)
|
||||
try {
|
||||
await this.storage.saveMetadata(id, null as any) // Clear metadata
|
||||
} catch {
|
||||
// Ignore if not supported
|
||||
}
|
||||
|
||||
// Delete related verbs
|
||||
// Get entity metadata and related verbs before deletion
|
||||
const metadata = await this.storage.getNounMetadata(id)
|
||||
const noun = await this.storage.getNoun(id)
|
||||
const verbs = await this.storage.getVerbsBySource(id)
|
||||
const targetVerbs = await this.storage.getVerbsByTarget(id)
|
||||
const allVerbs = [...verbs, ...targetVerbs]
|
||||
|
||||
for (const verb of allVerbs) {
|
||||
// Remove from graph index first
|
||||
await this.graphIndex.removeVerb(verb.id)
|
||||
// Then delete from storage
|
||||
await this.storage.deleteVerb(verb.id)
|
||||
// Delete verb metadata if exists
|
||||
try {
|
||||
if (typeof (this.storage as any).deleteVerbMetadata === 'function') {
|
||||
await (this.storage as any).deleteVerbMetadata(verb.id)
|
||||
// v5.8.0: Execute atomically with transaction system
|
||||
await this.transactionManager.executeTransaction(async (tx) => {
|
||||
// Operation 1: Remove from vector index
|
||||
if (noun && metadata) {
|
||||
if (this.index instanceof TypeAwareHNSWIndex && metadata.noun) {
|
||||
tx.addOperation(
|
||||
new RemoveFromTypeAwareHNSWOperation(
|
||||
this.index,
|
||||
id,
|
||||
noun.vector,
|
||||
metadata.noun as any
|
||||
)
|
||||
)
|
||||
} else if (this.index instanceof HNSWIndex || this.index instanceof HNSWIndexOptimized) {
|
||||
tx.addOperation(
|
||||
new RemoveFromHNSWOperation(this.index, id, noun.vector)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Ignore if not supported
|
||||
}
|
||||
}
|
||||
|
||||
// Operation 2: Remove from metadata index
|
||||
if (metadata) {
|
||||
tx.addOperation(
|
||||
new RemoveFromMetadataIndexOperation(this.metadataIndex, id, metadata)
|
||||
)
|
||||
}
|
||||
|
||||
// Operation 3: Delete noun metadata
|
||||
tx.addOperation(
|
||||
new DeleteNounMetadataOperation(this.storage, id)
|
||||
)
|
||||
|
||||
// Operations 4+: Delete all related verbs atomically
|
||||
for (const verb of allVerbs) {
|
||||
// Remove from graph index
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb as any)
|
||||
)
|
||||
// Delete verb metadata
|
||||
tx.addOperation(
|
||||
new DeleteVerbMetadataOperation(this.storage, verb.id)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -920,18 +988,21 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
// CRITICAL FIX (v3.43.2): Check for duplicate relationships
|
||||
// This prevents infinite loops where same relationship is created repeatedly
|
||||
// Bug #1 showed incrementing verb counts (7→8→9...) indicating duplicates
|
||||
const existingVerbs = await this.storage.getVerbsBySource(params.from)
|
||||
const duplicate = existingVerbs.find(v =>
|
||||
v.targetId === params.to &&
|
||||
v.verb === params.type
|
||||
)
|
||||
// v5.8.0 OPTIMIZATION: Use GraphAdjacencyIndex for O(log n) lookup instead of O(n) storage scan
|
||||
const verbIds = await this.graphIndex.getVerbIdsBySource(params.from)
|
||||
|
||||
if (duplicate) {
|
||||
// Relationship already exists - return existing ID instead of creating duplicate
|
||||
console.log(`[DEBUG] Skipping duplicate relationship: ${params.from} → ${params.to} (${params.type})`)
|
||||
return duplicate.id
|
||||
// Check each verb ID for matching relationship (only load verbs we need to check)
|
||||
for (const verbId of verbIds) {
|
||||
const verb = await this.graphIndex.getVerbCached(verbId)
|
||||
if (verb && verb.targetId === params.to && verb.verb === params.type) {
|
||||
// Relationship already exists - return existing ID instead of creating duplicate
|
||||
console.log(`[DEBUG] Skipping duplicate relationship: ${params.from} → ${params.to} (${params.type})`)
|
||||
return verb.id
|
||||
}
|
||||
}
|
||||
|
||||
// No duplicate found - proceed with creation
|
||||
|
||||
// Generate ID
|
||||
const id = uuidv4()
|
||||
|
||||
|
|
@ -965,47 +1036,66 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
createdAt: Date.now()
|
||||
} as any
|
||||
|
||||
await this.storage.saveVerb({
|
||||
id,
|
||||
vector: relationVector,
|
||||
connections: new Map(),
|
||||
verb: params.type,
|
||||
sourceId: params.from,
|
||||
targetId: params.to
|
||||
// v5.8.0: Execute atomically with transaction system
|
||||
await this.transactionManager.executeTransaction(async (tx) => {
|
||||
// Operation 1: Save verb vector data
|
||||
tx.addOperation(
|
||||
new SaveVerbOperation(this.storage, {
|
||||
id,
|
||||
vector: relationVector,
|
||||
connections: new Map(),
|
||||
verb: params.type,
|
||||
sourceId: params.from,
|
||||
targetId: params.to
|
||||
})
|
||||
)
|
||||
|
||||
// Operation 2: Save verb metadata
|
||||
tx.addOperation(
|
||||
new SaveVerbMetadataOperation(this.storage, id, verbMetadata)
|
||||
)
|
||||
|
||||
// Operation 3: Add to graph index for O(1) lookups
|
||||
tx.addOperation(
|
||||
new AddToGraphIndexOperation(this.graphIndex, verb)
|
||||
)
|
||||
|
||||
// Create bidirectional if requested
|
||||
if (params.bidirectional) {
|
||||
const reverseId = uuidv4()
|
||||
const reverseVerb: GraphVerb = {
|
||||
...verb,
|
||||
id: reverseId,
|
||||
sourceId: params.to,
|
||||
targetId: params.from,
|
||||
source: toEntity.type,
|
||||
target: fromEntity.type
|
||||
} as any
|
||||
|
||||
// Operation 4: Save reverse verb vector data
|
||||
tx.addOperation(
|
||||
new SaveVerbOperation(this.storage, {
|
||||
id: reverseId,
|
||||
vector: relationVector,
|
||||
connections: new Map(),
|
||||
verb: params.type,
|
||||
sourceId: params.to,
|
||||
targetId: params.from
|
||||
})
|
||||
)
|
||||
|
||||
// Operation 5: Save reverse verb metadata
|
||||
tx.addOperation(
|
||||
new SaveVerbMetadataOperation(this.storage, reverseId, verbMetadata)
|
||||
)
|
||||
|
||||
// Operation 6: Add reverse relationship to graph index
|
||||
tx.addOperation(
|
||||
new AddToGraphIndexOperation(this.graphIndex, reverseVerb)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await this.storage.saveVerbMetadata(id, verbMetadata)
|
||||
|
||||
// Add to graph index for O(1) lookups
|
||||
await this.graphIndex.addVerb(verb)
|
||||
|
||||
// Create bidirectional if requested
|
||||
if (params.bidirectional) {
|
||||
const reverseId = uuidv4()
|
||||
const reverseVerb: GraphVerb = {
|
||||
...verb,
|
||||
id: reverseId,
|
||||
sourceId: params.to,
|
||||
targetId: params.from,
|
||||
source: toEntity.type,
|
||||
target: fromEntity.type
|
||||
} as any
|
||||
|
||||
await this.storage.saveVerb({
|
||||
id: reverseId,
|
||||
vector: relationVector,
|
||||
connections: new Map(),
|
||||
verb: params.type,
|
||||
sourceId: params.to,
|
||||
targetId: params.from
|
||||
})
|
||||
|
||||
await this.storage.saveVerbMetadata(reverseId, verbMetadata)
|
||||
|
||||
// Add reverse relationship to graph index too
|
||||
await this.graphIndex.addVerb(reverseVerb)
|
||||
}
|
||||
|
||||
return id
|
||||
})
|
||||
}
|
||||
|
|
@ -1017,10 +1107,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
await this.ensureInitialized()
|
||||
|
||||
return this.augmentationRegistry.execute('unrelate', { id }, async () => {
|
||||
// Remove from graph index
|
||||
await this.graphIndex.removeVerb(id)
|
||||
// Remove from storage
|
||||
await this.storage.deleteVerb(id)
|
||||
// Get verb data before deletion for rollback
|
||||
const verb = await this.storage.getVerb(id)
|
||||
|
||||
// v5.8.0: Execute atomically with transaction system
|
||||
await this.transactionManager.executeTransaction(async (tx) => {
|
||||
// Operation 1: Remove from graph index
|
||||
if (verb) {
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb as any)
|
||||
)
|
||||
}
|
||||
|
||||
// Operation 2: Delete verb metadata (which also deletes vector)
|
||||
tx.addOperation(
|
||||
new DeleteVerbMetadataOperation(this.storage, id)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,12 +136,48 @@ export class GraphAdjacencyIndex {
|
|||
|
||||
/**
|
||||
* Core API - Neighbor lookup with LSM-tree storage
|
||||
* Now O(log n) with bloom filter optimization (90% of queries skip disk I/O)
|
||||
*
|
||||
* O(log n) with bloom filter optimization (90% of queries skip disk I/O)
|
||||
* v5.8.0: Added pagination support for high-degree nodes
|
||||
*
|
||||
* @param id Entity ID to get neighbors for
|
||||
* @param optionsOrDirection Optional: direction string OR options object
|
||||
* @returns Array of neighbor IDs (paginated if limit/offset specified)
|
||||
*
|
||||
* @example
|
||||
* // Get all neighbors (backward compatible)
|
||||
* const all = await graphIndex.getNeighbors(id)
|
||||
*
|
||||
* @example
|
||||
* // Get outgoing neighbors (backward compatible)
|
||||
* const out = await graphIndex.getNeighbors(id, 'out')
|
||||
*
|
||||
* @example
|
||||
* // Get first 50 outgoing neighbors (new API)
|
||||
* const page1 = await graphIndex.getNeighbors(id, { direction: 'out', limit: 50 })
|
||||
*
|
||||
* @example
|
||||
* // Paginate through neighbors
|
||||
* const page1 = await graphIndex.getNeighbors(id, { limit: 100, offset: 0 })
|
||||
* const page2 = await graphIndex.getNeighbors(id, { limit: 100, offset: 100 })
|
||||
*/
|
||||
async getNeighbors(id: string, direction?: 'in' | 'out' | 'both'): Promise<string[]> {
|
||||
async getNeighbors(
|
||||
id: string,
|
||||
optionsOrDirection?: {
|
||||
direction?: 'in' | 'out' | 'both'
|
||||
limit?: number
|
||||
offset?: number
|
||||
} | 'in' | 'out' | 'both'
|
||||
): Promise<string[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Normalize old API (direction string) to new API (options object)
|
||||
const options = typeof optionsOrDirection === 'string'
|
||||
? { direction: optionsOrDirection }
|
||||
: (optionsOrDirection || {})
|
||||
|
||||
const startTime = performance.now()
|
||||
const direction = options.direction || 'both'
|
||||
const neighbors = new Set<string>()
|
||||
|
||||
// Query LSM-trees with bloom filter optimization
|
||||
|
|
@ -159,7 +195,16 @@ export class GraphAdjacencyIndex {
|
|||
}
|
||||
}
|
||||
|
||||
const result = Array.from(neighbors)
|
||||
// Convert to array for pagination
|
||||
let result = Array.from(neighbors)
|
||||
|
||||
// Apply pagination if requested (v5.8.0)
|
||||
if (options?.limit !== undefined || options?.offset !== undefined) {
|
||||
const offset = options.offset || 0
|
||||
const limit = options.limit !== undefined ? options.limit : result.length
|
||||
result = result.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
const elapsed = performance.now() - startTime
|
||||
|
||||
// Performance assertion - should be sub-5ms with LSM-tree
|
||||
|
|
@ -172,13 +217,37 @@ export class GraphAdjacencyIndex {
|
|||
|
||||
/**
|
||||
* Get verb IDs by source - Billion-scale optimization for getVerbsBySource
|
||||
*
|
||||
* O(log n) LSM-tree lookup with bloom filter optimization
|
||||
* v5.7.1: Filters out deleted verb IDs (tombstone deletion workaround)
|
||||
* v5.8.0: Added pagination support for entities with many relationships
|
||||
*
|
||||
* @param sourceId Source entity ID
|
||||
* @returns Array of verb IDs originating from this source (excluding deleted)
|
||||
* @param options Optional configuration
|
||||
* @param options.limit Maximum number of verb IDs to return (default: all)
|
||||
* @param options.offset Number of verb IDs to skip (default: 0)
|
||||
* @returns Array of verb IDs originating from this source (excluding deleted, paginated if requested)
|
||||
*
|
||||
* @example
|
||||
* // Get all verb IDs (backward compatible)
|
||||
* const all = await graphIndex.getVerbIdsBySource(sourceId)
|
||||
*
|
||||
* @example
|
||||
* // Get first 50 verb IDs
|
||||
* const page1 = await graphIndex.getVerbIdsBySource(sourceId, { limit: 50 })
|
||||
*
|
||||
* @example
|
||||
* // Paginate through verb IDs
|
||||
* const page1 = await graphIndex.getVerbIdsBySource(sourceId, { limit: 100, offset: 0 })
|
||||
* const page2 = await graphIndex.getVerbIdsBySource(sourceId, { limit: 100, offset: 100 })
|
||||
*/
|
||||
async getVerbIdsBySource(sourceId: string): Promise<string[]> {
|
||||
async getVerbIdsBySource(
|
||||
sourceId: string,
|
||||
options?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
): Promise<string[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const startTime = performance.now()
|
||||
|
|
@ -193,18 +262,51 @@ export class GraphAdjacencyIndex {
|
|||
// Filter out deleted verb IDs (tombstone deletion workaround)
|
||||
// LSM-tree retains all IDs, but verbIdSet tracks deletions
|
||||
const allIds = verbIds || []
|
||||
return allIds.filter(id => this.verbIdSet.has(id))
|
||||
let result = allIds.filter(id => this.verbIdSet.has(id))
|
||||
|
||||
// Apply pagination if requested (v5.8.0)
|
||||
if (options?.limit !== undefined || options?.offset !== undefined) {
|
||||
const offset = options.offset || 0
|
||||
const limit = options.limit !== undefined ? options.limit : result.length
|
||||
result = result.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verb IDs by target - Billion-scale optimization for getVerbsByTarget
|
||||
*
|
||||
* O(log n) LSM-tree lookup with bloom filter optimization
|
||||
* v5.7.1: Filters out deleted verb IDs (tombstone deletion workaround)
|
||||
* v5.8.0: Added pagination support for popular target entities
|
||||
*
|
||||
* @param targetId Target entity ID
|
||||
* @returns Array of verb IDs pointing to this target (excluding deleted)
|
||||
* @param options Optional configuration
|
||||
* @param options.limit Maximum number of verb IDs to return (default: all)
|
||||
* @param options.offset Number of verb IDs to skip (default: 0)
|
||||
* @returns Array of verb IDs pointing to this target (excluding deleted, paginated if requested)
|
||||
*
|
||||
* @example
|
||||
* // Get all verb IDs (backward compatible)
|
||||
* const all = await graphIndex.getVerbIdsByTarget(targetId)
|
||||
*
|
||||
* @example
|
||||
* // Get first 50 verb IDs
|
||||
* const page1 = await graphIndex.getVerbIdsByTarget(targetId, { limit: 50 })
|
||||
*
|
||||
* @example
|
||||
* // Paginate through verb IDs
|
||||
* const page1 = await graphIndex.getVerbIdsByTarget(targetId, { limit: 100, offset: 0 })
|
||||
* const page2 = await graphIndex.getVerbIdsByTarget(targetId, { limit: 100, offset: 100 })
|
||||
*/
|
||||
async getVerbIdsByTarget(targetId: string): Promise<string[]> {
|
||||
async getVerbIdsByTarget(
|
||||
targetId: string,
|
||||
options?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
): Promise<string[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const startTime = performance.now()
|
||||
|
|
@ -219,7 +321,16 @@ export class GraphAdjacencyIndex {
|
|||
// Filter out deleted verb IDs (tombstone deletion workaround)
|
||||
// LSM-tree retains all IDs, but verbIdSet tracks deletions
|
||||
const allIds = verbIds || []
|
||||
return allIds.filter(id => this.verbIdSet.has(id))
|
||||
let result = allIds.filter(id => this.verbIdSet.has(id))
|
||||
|
||||
// Apply pagination if requested (v5.8.0)
|
||||
if (options?.limit !== undefined || options?.offset !== undefined) {
|
||||
const offset = options.offset || 0
|
||||
const limit = options.limit !== undefined ? options.limit : result.length
|
||||
result = result.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
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
|
||||
}
|
||||
}
|
||||
189
src/transaction/TransactionManager.ts
Normal file
189
src/transaction/TransactionManager.ts
Normal 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
88
src/transaction/errors.ts
Normal 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
32
src/transaction/index.ts
Normal 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'
|
||||
361
src/transaction/operations/IndexOperations.ts
Normal file
361
src/transaction/operations/IndexOperations.ts
Normal 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]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
301
src/transaction/operations/StorageOperations.ts
Normal file
301
src/transaction/operations/StorageOperations.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
34
src/transaction/operations/index.ts
Normal file
34
src/transaction/operations/index.ts
Normal 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
103
src/transaction/types.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue