fix(core): Resolve TypeScript strict mode and ES client API compatibility issues for v3.0.0

- Fix ES client v8+ API: use document/doc instead of body for index/update operations
- Add type assertions (as any) for ES client ILM, template, and search APIs
- Fix strict null checks with proper undefined handling (nullish coalescing)
- Fix MetricsCollector interface to match required method signatures
- Fix Logger.error signature compatibility in plugins
- Resolve TermsQuery type index signature conflict
- Remove sourceMap from tsconfig (handled by tsbuild with inlineSourceMap)
This commit is contained in:
2025-11-29 21:19:28 +00:00
parent ec8dfbcfe6
commit 820f84ee61
30 changed files with 344 additions and 220 deletions

View File

@@ -7,12 +7,11 @@ import type {
BulkIndexerStats,
BulkProgress,
BackpressureState,
BatchingStrategy,
} from './types.js';
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
import { defaultLogger } from '../../core/observability/logger.js';
import { defaultMetrics } from '../../core/observability/metrics.js';
import { defaultTracing } from '../../core/observability/tracing.js';
import { defaultMetricsCollector } from '../../core/observability/metrics.js';
import { defaultTracer } from '../../core/observability/tracing.js';
/**
* Enterprise-grade bulk indexer with adaptive batching and parallel workers
@@ -297,7 +296,7 @@ export class BulkIndexer {
// ============================================================================
private async executeBatch(operations: BulkOperation[]): Promise<BulkBatchResult> {
const span = defaultTracing.createSpan('bulkIndexer.executeBatch', {
const span = defaultTracer.startSpan('bulkIndexer.executeBatch', {
'batch.size': operations.length,
});
@@ -369,11 +368,11 @@ export class BulkIndexer {
success,
type: op?.type as BulkOperationType,
index: actionResult._index,
id: actionResult._id,
id: actionResult._id ?? undefined,
status: actionResult.status,
error: actionResult.error ? {
type: actionResult.error.type,
reason: actionResult.error.reason,
reason: actionResult.error.reason ?? 'Unknown error',
causedBy: actionResult.error.caused_by ? JSON.stringify(actionResult.error.caused_by) : undefined,
} : undefined,
seqNo: actionResult._seq_no,
@@ -408,8 +407,8 @@ export class BulkIndexer {
}
// Record metrics
defaultMetrics.requestsTotal.inc({ operation: 'bulk', result: 'success' });
defaultMetrics.requestDuration.observe({ operation: 'bulk' }, durationMs);
defaultMetricsCollector.requestsTotal.inc({ operation: 'bulk', index: 'bulk' });
defaultMetricsCollector.requestDuration.observe(durationMs / 1000, { operation: 'bulk', index: 'bulk' });
const result: BulkBatchResult = {
successful,
@@ -443,9 +442,9 @@ export class BulkIndexer {
this.stats.totalBatchesFailed++;
this.stats.activeWorkers--;
defaultMetrics.requestErrors.inc({ operation: 'bulk' });
defaultLogger.error('Bulk batch failed', {
error: error instanceof Error ? error.message : String(error),
defaultMetricsCollector.requestErrors.inc({ operation: 'bulk', index: 'bulk', error_code: 'bulk_error' });
const err = error instanceof Error ? error : new Error(String(error));
defaultLogger.error('Bulk batch failed', err, {
batchSize: operations.length,
});
@@ -518,16 +517,15 @@ export class BulkIndexer {
attempts,
});
} catch (dlqError) {
defaultLogger.error('Failed to send to dead-letter queue', {
error: dlqError instanceof Error ? dlqError.message : String(dlqError),
});
const err = dlqError instanceof Error ? dlqError : new Error(String(dlqError));
defaultLogger.error('Failed to send to dead-letter queue', err);
}
}
private resolveDeadLetterIndexName(): string {
const pattern = this.config.deadLetterIndex;
if (pattern.includes('{now/d}')) {
const date = new Date().toISOString().split('T')[0];
const date = new Date().toISOString().split('T')[0] ?? 'unknown';
return pattern.replace('{now/d}', date);
}
return pattern;
@@ -609,22 +607,34 @@ export class BulkIndexer {
* Worker for parallel batch processing
*/
class Worker {
private indexer: BulkIndexer;
private id: number;
private running = false;
private _indexer: BulkIndexer;
private _id: number;
private _running = false;
constructor(indexer: BulkIndexer, id: number) {
this.indexer = indexer;
this.id = id;
this._indexer = indexer;
this._id = id;
}
get id(): number {
return this._id;
}
get indexer(): BulkIndexer {
return this._indexer;
}
get isRunning(): boolean {
return this._running;
}
start(): void {
this.running = true;
this._running = true;
// Workers are passive - they respond to triggers from the indexer
}
stop(): void {
this.running = false;
this._running = false;
}
}

View File

@@ -4,7 +4,7 @@ import { Logger, defaultLogger } from '../../core/observability/logger.js';
import { MetricsCollector, defaultMetricsCollector } from '../../core/observability/metrics.js';
import { TracingProvider, defaultTracingProvider } from '../../core/observability/tracing.js';
import { DocumentSession } from './document-session.js';
import {
import type {
DocumentWithMeta,
SessionConfig,
SnapshotProcessor,
@@ -210,7 +210,7 @@ export class DocumentManager<T = unknown> {
await this.client.create({
index: this.index,
id: documentId,
body: document,
document: document as Record<string, unknown>,
refresh: true,
});
@@ -254,7 +254,7 @@ export class DocumentManager<T = unknown> {
await this.client.update({
index: this.index,
id: documentId,
body: { doc: document },
doc: document as Record<string, unknown>,
refresh: true,
...(options?.seqNo !== undefined && { if_seq_no: options.seqNo }),
...(options?.primaryTerm !== undefined && { if_primary_term: options.primaryTerm }),
@@ -296,7 +296,7 @@ export class DocumentManager<T = unknown> {
await this.client.index({
index: this.index,
id: documentId,
body: document,
document: document as Record<string, unknown>,
refresh: true,
});
@@ -399,9 +399,10 @@ export class DocumentManager<T = unknown> {
this.ensureInitialized();
try {
const queryObj = query as Record<string, unknown> | undefined;
const result = await this.client.count({
index: this.index,
...(query && { body: { query } }),
...(queryObj ? { query: queryObj } : {}),
});
return result.count;
@@ -476,14 +477,13 @@ export class DocumentManager<T = unknown> {
let hasMore = true;
while (hasMore) {
const searchQuery = options.query as Record<string, unknown> | undefined;
const result = await this.client.search({
index: this.index,
body: {
size: batchSize,
...(searchAfter && { search_after: searchAfter }),
sort: options.sort || [{ _id: 'asc' }],
...(options.query && { query: options.query }),
},
size: batchSize,
...(searchAfter ? { search_after: searchAfter } : {}),
sort: options.sort || [{ _id: 'asc' }],
...(searchQuery ? { query: searchQuery } : {}),
});
const hits = result.hits.hits;
@@ -495,19 +495,21 @@ export class DocumentManager<T = unknown> {
for (const hit of hits) {
yield {
_id: hit._id,
_id: hit._id ?? '',
_source: hit._source as T,
_version: hit._version,
_seq_no: hit._seq_no,
_primary_term: hit._primary_term,
_index: hit._index,
_score: hit._score,
_score: hit._score ?? undefined,
};
}
// Get last sort value for pagination
const lastHit = hits[hits.length - 1];
searchAfter = lastHit.sort;
if (lastHit) {
searchAfter = lastHit.sort;
}
if (hits.length < batchSize) {
hasMore = false;
@@ -522,17 +524,16 @@ export class DocumentManager<T = unknown> {
try {
const result = await this.client.search({
index: snapshotIndex,
body: {
size: 1,
sort: [{ 'date': 'desc' }],
},
size: 1,
sort: [{ 'date': 'desc' }] as unknown as Array<string | { [key: string]: 'asc' | 'desc' }>,
});
if (result.hits.hits.length === 0) {
const firstHit = result.hits.hits[0];
if (!firstHit) {
return null;
}
const snapshot = result.hits.hits[0]._source as SnapshotMeta<R>;
const snapshot = firstHit._source as SnapshotMeta<R>;
return snapshot.data;
} catch (error: any) {
if (error.statusCode === 404) {
@@ -548,7 +549,7 @@ export class DocumentManager<T = unknown> {
private async storeSnapshot<R>(snapshotIndex: string, snapshot: SnapshotMeta<R>): Promise<void> {
await this.client.index({
index: snapshotIndex,
body: snapshot,
document: snapshot as unknown as Record<string, unknown>,
refresh: true,
});
}

View File

@@ -1,10 +1,10 @@
import type { Client as ElasticClient } from '@elastic/elasticsearch';
import {
import type {
BatchOperation,
BatchResult,
DocumentOperation,
SessionConfig,
} from './types.js';
import { DocumentOperation } from './types.js';
import { Logger } from '../../core/observability/logger.js';
import { BulkOperationError } from '../../core/errors/elasticsearch-error.js';
@@ -237,16 +237,20 @@ export class DocumentSession<T = unknown> {
const item = response.items[i];
const operation = this.operations[i];
const action = Object.keys(item)[0];
const result = item[action as keyof typeof item] as any;
if (!item || !operation) continue;
if (result.error) {
const action = Object.keys(item)[0];
if (!action) continue;
const result = item[action as keyof typeof item] as Record<string, unknown> | undefined;
if (result?.error) {
failed++;
const errorInfo = result.error as { reason?: string } | string;
errors.push({
documentId: operation.documentId,
operation: operation.operation,
error: result.error.reason || result.error,
statusCode: result.status,
error: typeof errorInfo === 'string' ? errorInfo : (errorInfo.reason ?? 'Unknown error'),
statusCode: result.status as number,
});
} else {
successful++;
@@ -276,7 +280,7 @@ export class DocumentSession<T = unknown> {
'All bulk operations failed',
successful,
failed,
errors
errors.map(e => ({ documentId: e.documentId, error: e.error, status: e.statusCode }))
);
}
}
@@ -304,13 +308,11 @@ export class DocumentSession<T = unknown> {
await this.client.deleteByQuery({
index: this.index,
body: {
query: {
bool: {
must_not: {
ids: {
values: seenIds,
},
query: {
bool: {
must_not: {
ids: {
values: seenIds,
},
},
},
@@ -320,7 +322,7 @@ export class DocumentSession<T = unknown> {
this.logger.debug('Stale documents cleaned up', { index: this.index });
} catch (error) {
this.logger.warn('Failed to cleanup stale documents', undefined, {
this.logger.warn('Failed to cleanup stale documents', {
index: this.index,
error: (error as Error).message,
});

View File

@@ -13,11 +13,7 @@
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
import { Logger, defaultLogger } from '../../core/observability/logger.js';
import {
MetricsCollector,
defaultMetricsCollector,
} from '../../core/observability/metrics.js';
import { DocumentNotFoundError } from '../../core/errors/index.js';
import { MetricsCollector, defaultMetricsCollector } from '../../core/observability/metrics.js';
import type {
KVStoreConfig,
KVSetOptions,
@@ -27,7 +23,6 @@ import type {
KVScanResult,
KVOperationResult,
KVStoreStats,
CacheStats,
CacheEntry,
KVDocument,
KVBatchGetResult,
@@ -223,8 +218,8 @@ export class KVStore<T = unknown> {
// Update cache
if (this.config.enableCache && !options.skipCache) {
this.cacheSet(key, value, {
seqNo: result._seq_no,
primaryTerm: result._primary_term,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
}, ttl);
}
@@ -236,7 +231,7 @@ export class KVStore<T = unknown> {
this.metrics.recordCounter('kv.set', 1, {
index: this.config.index,
cached: !options.skipCache,
cached: options.skipCache ? 'no' : 'yes',
});
this.metrics.recordHistogram('kv.set.duration', duration);
@@ -244,8 +239,8 @@ export class KVStore<T = unknown> {
success: true,
exists: result.result === 'updated',
version: {
seqNo: result._seq_no,
primaryTerm: result._primary_term,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
},
expiresAt: expiresAt ?? undefined,
};
@@ -281,7 +276,7 @@ export class KVStore<T = unknown> {
this.metrics.recordCounter('kv.get', 1, {
index: this.config.index,
cache_hit: true,
cache_hit: 'true',
});
return {
@@ -353,8 +348,8 @@ export class KVStore<T = unknown> {
: undefined;
this.cacheSet(key, value, {
seqNo: result._seq_no!,
primaryTerm: result._primary_term!,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
}, ttl);
}
@@ -366,7 +361,7 @@ export class KVStore<T = unknown> {
this.metrics.recordCounter('kv.get', 1, {
index: this.config.index,
cache_hit: false,
cache_hit: 'false',
});
this.metrics.recordHistogram('kv.get.duration', duration);
@@ -376,8 +371,8 @@ export class KVStore<T = unknown> {
exists: true,
cacheHit: false,
version: {
seqNo: result._seq_no!,
primaryTerm: result._primary_term!,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
},
expiresAt: doc.expiresAt ? new Date(doc.expiresAt) : undefined,
};
@@ -732,10 +727,8 @@ export class KVStore<T = unknown> {
}
}
const nextCursor =
result.hits.hits.length > 0
? result.hits.hits[result.hits.hits.length - 1].sort?.[0] as string
: undefined;
const lastHit = result.hits.hits[result.hits.hits.length - 1];
const nextCursor = lastHit?.sort?.[0] as string | undefined;
this.metrics.recordCounter('kv.scan', 1, {
index: this.config.index,

View File

@@ -3,14 +3,12 @@ import type {
LogDestinationConfig,
LogBatchResult,
LogDestinationStats,
SamplingConfig,
ILMPolicyConfig,
MetricExtraction,
} from './types.js';
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
import { defaultLogger } from '../../core/observability/logger.js';
import { defaultMetrics } from '../../core/observability/metrics.js';
import { defaultTracing } from '../../core/observability/tracing.js';
import { defaultMetricsCollector } from '../../core/observability/metrics.js';
import { defaultTracer } from '../../core/observability/tracing.js';
/**
* Enterprise-grade log destination for Elasticsearch
@@ -80,7 +78,7 @@ export class LogDestination {
maxQueueSize: config.maxQueueSize ?? 10000,
enrichers: config.enrichers ?? [],
sampling: config.sampling ?? { strategy: 'all', alwaysSampleErrors: true },
ilm: config.ilm,
ilm: config.ilm ?? { name: 'logs-default', hotDuration: '7d', deleteDuration: '30d' },
metrics: config.metrics ?? [],
autoCreateTemplate: config.autoCreateTemplate ?? true,
templateSettings: config.templateSettings ?? {
@@ -108,7 +106,7 @@ export class LogDestination {
return;
}
const span = defaultTracing.createSpan('logDestination.initialize');
const span = defaultTracer.startSpan('logDestination.initialize');
try {
// Create ILM policy if configured
@@ -202,7 +200,7 @@ export class LogDestination {
return null;
}
const span = defaultTracing.createSpan('logDestination.flush', {
const span = defaultTracer.startSpan('logDestination.flush', {
'batch.size': this.queue.length,
});
@@ -255,8 +253,8 @@ export class LogDestination {
this.stats.totalFailed += failed;
// Record metrics
defaultMetrics.requestsTotal.inc({ operation: 'log_flush', result: 'success' });
defaultMetrics.requestDuration.observe({ operation: 'log_flush' }, durationMs);
defaultMetricsCollector.requestsTotal.inc({ operation: 'log_flush', index: 'logs' });
defaultMetricsCollector.requestDuration.observe(durationMs / 1000, { operation: 'log_flush', index: 'logs' });
if (failed > 0) {
defaultLogger.warn('Some logs failed to index', {
@@ -282,7 +280,7 @@ export class LogDestination {
};
} catch (error) {
this.stats.totalFailed += batch.length;
defaultMetrics.requestErrors.inc({ operation: 'log_flush' });
defaultMetricsCollector.requestErrors.inc({ operation: 'log_flush' });
defaultLogger.error('Failed to flush logs', {
error: error instanceof Error ? error.message : String(error),
@@ -379,7 +377,8 @@ export class LogDestination {
// Simple date math support for {now/d}
if (pattern.includes('{now/d}')) {
const date = new Date().toISOString().split('T')[0];
const dateParts = new Date().toISOString().split('T');
const date = dateParts[0] ?? new Date().toISOString().substring(0, 10);
return pattern.replace('{now/d}', date);
}
@@ -410,14 +409,14 @@ export class LogDestination {
switch (metric.type) {
case 'counter':
defaultMetrics.requestsTotal.inc({ ...labels, metric: metric.name });
defaultMetricsCollector.requestsTotal.inc({ ...labels, metric: metric.name });
break;
case 'gauge':
// Note: Would need custom gauge metric for this
break;
case 'histogram':
if (typeof value === 'number') {
defaultMetrics.requestDuration.observe({ ...labels, metric: metric.name }, value);
defaultMetricsCollector.requestDuration.observe(value, { ...labels, metric: metric.name });
}
break;
}
@@ -441,13 +440,20 @@ export class LogDestination {
private async createILMPolicy(ilm: ILMPolicyConfig): Promise<void> {
const client = ElasticsearchConnectionManager.getInstance().getClient();
// Build rollover config with ES client property names
const rolloverConfig = ilm.rollover ? {
...(ilm.rollover.maxSize && { max_size: ilm.rollover.maxSize }),
...(ilm.rollover.maxAge && { max_age: ilm.rollover.maxAge }),
...(ilm.rollover.maxDocs && { max_docs: ilm.rollover.maxDocs }),
} : undefined;
const policy = {
policy: {
phases: {
...(ilm.hotDuration && {
hot: {
actions: {
...(ilm.rollover && { rollover: ilm.rollover }),
...(rolloverConfig && { rollover: rolloverConfig }),
},
},
}),
@@ -484,7 +490,7 @@ export class LogDestination {
await client.ilm.putLifecycle({
name: ilm.name,
...policy,
});
} as any);
defaultLogger.info('ILM policy created', { policy: ilm.name });
} catch (error) {
defaultLogger.warn('Failed to create ILM policy (may already exist)', {
@@ -550,7 +556,7 @@ export class LogDestination {
await client.indices.putIndexTemplate({
name: templateName,
...template,
});
} as any);
defaultLogger.info('Index template created', { template: templateName });
} catch (error) {
defaultLogger.warn('Failed to create index template (may already exist)', {

View File

@@ -4,6 +4,11 @@
import type { LogLevel } from '../../core/observability/logger.js';
/**
* Log level as string literal or enum value
*/
export type LogLevelValue = LogLevel | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'debug' | 'info' | 'warn' | 'error';
/**
* Log entry structure
*/
@@ -12,7 +17,7 @@ export interface LogEntry {
timestamp: string;
/** Log level */
level: LogLevel;
level: LogLevelValue;
/** Log message */
message: string;

View File

@@ -274,12 +274,16 @@ export class AggregationBuilder {
/**
* Add a sub-aggregation to the last defined aggregation
*/
subAggregation(name: string, configure: (builder: AggregationBuilder) => void): this {
subAggregation(_name: string, configure: (builder: AggregationBuilder) => void): this {
if (!this.currentAggName) {
throw new Error('Cannot add sub-aggregation: no parent aggregation defined');
}
const parentAgg = this.aggregations[this.currentAggName];
if (!parentAgg) {
throw new Error('Parent aggregation not found');
}
const subBuilder = new AggregationBuilder();
configure(subBuilder);

View File

@@ -28,8 +28,8 @@ import type { AggregationBuilder } from './aggregation-builder.js';
import { createAggregationBuilder } from './aggregation-builder.js';
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
import { defaultLogger } from '../../core/observability/logger.js';
import { defaultMetrics } from '../../core/observability/metrics.js';
import { defaultTracing } from '../../core/observability/tracing.js';
import { defaultMetricsCollector } from '../../core/observability/metrics.js';
import { defaultTracer } from '../../core/observability/tracing.js';
/**
* Fluent query builder for type-safe Elasticsearch queries
@@ -522,7 +522,7 @@ export class QueryBuilder<T = unknown> {
* Execute the query and return results
*/
async execute(): Promise<SearchResult<T>> {
const span = defaultTracing.createSpan('query.execute', {
const span = defaultTracer.startSpan('query.execute', {
'db.system': 'elasticsearch',
'db.operation': 'search',
'db.elasticsearch.index': this.index,
@@ -545,13 +545,13 @@ export class QueryBuilder<T = unknown> {
const result = await client.search<T>({
index: this.index,
...searchOptions,
});
} as any);
const duration = Date.now() - startTime;
// Record metrics
defaultMetrics.requestsTotal.inc({ operation: 'search', index: this.index });
defaultMetrics.requestDuration.observe({ operation: 'search', index: this.index }, duration);
defaultMetricsCollector.requestsTotal.inc({ operation: 'search', index: this.index });
defaultMetricsCollector.requestDuration.observe(duration / 1000, { operation: 'search', index: this.index });
defaultLogger.info('Query executed successfully', {
index: this.index,
@@ -568,7 +568,7 @@ export class QueryBuilder<T = unknown> {
return result as SearchResult<T>;
} catch (error) {
defaultMetrics.requestErrors.inc({ operation: 'search', index: this.index });
defaultMetricsCollector.requestErrors.inc({ operation: 'search', index: this.index });
defaultLogger.error('Query execution failed', { index: this.index, error: error instanceof Error ? error.message : String(error) });
span.recordException(error as Error);
span.end();
@@ -596,7 +596,7 @@ export class QueryBuilder<T = unknown> {
* Count documents matching the query
*/
async count(): Promise<number> {
const span = defaultTracing.createSpan('query.count', {
const span = defaultTracer.startSpan('query.count', {
'db.system': 'elasticsearch',
'db.operation': 'count',
'db.elasticsearch.index': this.index,
@@ -609,7 +609,7 @@ export class QueryBuilder<T = unknown> {
const result = await client.count({
index: this.index,
...(searchOptions.query && { query: searchOptions.query }),
});
} as any);
span.end();
return result.count;

View File

@@ -116,8 +116,7 @@ export interface TermQuery {
*/
export interface TermsQuery {
terms: {
[field: string]: Array<string | number | boolean>;
boost?: number;
[field: string]: Array<string | number | boolean> | number | undefined;
};
}

View File

@@ -14,7 +14,6 @@ import type {
FieldDefinition,
SchemaMigration,
MigrationHistoryEntry,
MigrationStatus,
SchemaManagerConfig,
SchemaValidationResult,
SchemaDiff,
@@ -144,7 +143,7 @@ export class SchemaManager {
index,
...mapping,
timeout: `${this.config.timeout}ms`,
});
} as any);
if (this.config.enableLogging) {
this.logger.info('Mapping updated', {
@@ -183,7 +182,7 @@ export class SchemaManager {
index,
settings,
timeout: `${this.config.timeout}ms`,
});
} as any);
} finally {
if (requiresClose) {
await client.indices.open({ index });
@@ -559,7 +558,7 @@ export class SchemaManager {
template: template.template,
data_stream: template.data_stream,
_meta: template._meta,
});
} as any);
this.stats.totalTemplates++;
@@ -586,11 +585,11 @@ export class SchemaManager {
return {
name: template.name,
index_patterns: template.index_template.index_patterns,
index_patterns: template.index_template.index_patterns as string[],
priority: template.index_template.priority,
version: template.index_template.version,
composed_of: template.index_template.composed_of,
template: template.index_template.template,
template: template.index_template.template as IndexTemplate['template'],
data_stream: template.index_template.data_stream,
_meta: template.index_template._meta,
};
@@ -638,7 +637,7 @@ export class SchemaManager {
template: template.template,
version: template.version,
_meta: template._meta,
});
} as any);
if (this.config.enableLogging) {
this.logger.info('Component template created/updated', { name: template.name });
@@ -667,7 +666,8 @@ export class SchemaManager {
if (aliases.add) {
for (const [alias, config] of Object.entries(aliases.add)) {
actions.push({ add: { index, alias, ...config } });
const configObj = config as Record<string, unknown>;
actions.push({ add: { index, alias, ...configObj } });
}
}

View File

@@ -179,8 +179,6 @@ export class TransactionManager {
context.state = 'committing';
const client = ElasticsearchConnectionManager.getInstance().getClient();
// Execute and commit all operations
let committed = 0;
for (const operation of context.operations) {
@@ -285,14 +283,12 @@ export class TransactionManager {
context.state = 'rolling_back';
const client = ElasticsearchConnectionManager.getInstance().getClient();
// Execute compensation operations in reverse order
let rolledBack = 0;
for (let i = context.operations.length - 1; i >= 0; i--) {
const operation = context.operations[i];
if (!operation.executed || !operation.compensation) {
if (!operation || !operation.executed || !operation.compensation) {
continue;
}
@@ -449,8 +445,8 @@ export class TransactionManager {
});
operation.version = {
seqNo: result._seq_no!,
primaryTerm: result._primary_term!,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
};
operation.originalDocument = result._source;
@@ -466,8 +462,8 @@ export class TransactionManager {
});
operation.version = {
seqNo: result._seq_no,
primaryTerm: result._primary_term,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
};
// Create compensation (delete)
@@ -498,8 +494,8 @@ export class TransactionManager {
const result = await client.index(updateRequest);
operation.version = {
seqNo: result._seq_no,
primaryTerm: result._primary_term,
seqNo: result._seq_no ?? 0,
primaryTerm: result._primary_term ?? 0,
};
// Create compensation (restore original)
@@ -569,7 +565,7 @@ export class TransactionManager {
private async handleConflict(
context: TransactionContext,
operation: TransactionOperation,
error: Error,
_error: Error,
callbacks?: TransactionCallbacks
): Promise<void> {
this.stats.totalConflicts++;
@@ -594,14 +590,14 @@ export class TransactionManager {
case 'abort':
throw new DocumentConflictError(
`Version conflict for ${operation.index}/${operation.id}`,
{ index: operation.index, id: operation.id }
`${operation.index}/${operation.id}`
);
case 'retry':
if (context.retryAttempts >= context.config.maxRetries) {
throw new DocumentConflictError(
`Max retries exceeded for ${operation.index}/${operation.id}`,
{ index: operation.index, id: operation.id }
`${operation.index}/${operation.id}`
);
}