UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

235 lines 7.84 kB
/** * Transformer registry and execution engine */ export class TransformerEngine { transformers = new Map(); maxChainLength; constructor(maxChainLength = 10) { this.maxChainLength = maxChainLength; } /** * Register a transformer pair (adjacent versions only) */ register(transformer) { // Validate immutability if (!transformer.immutable) { throw new Error('Transformer must be marked as immutable'); } // Create bidirectional keys const forwardKey = this.makeKey(transformer.fromVersion, transformer.toVersion); const backwardKey = this.makeKey(transformer.toVersion, transformer.fromVersion); // Check if already registered (immutable - cannot override) if (this.transformers.has(forwardKey)) { throw new Error(`Transformer from ${transformer.fromVersion} to ${transformer.toVersion} already exists and is immutable`); } // Store transformer this.transformers.set(forwardKey, transformer); // Also store reverse reference for backward lookup this.transformers.set(backwardKey, transformer); } /** * Get transformer between two adjacent versions */ getTransformer(from, to) { const key = this.makeKey(from, to); return this.transformers.get(key); } /** * Check if transformer exists */ hasTransformer(from, to) { return this.transformers.has(this.makeKey(from, to)); } /** * Build transformation chain from source to target version * Returns linear chain (no circular dependencies possible) */ buildChain(from, to, versions) { // Same version - no transformation needed if (from === to) { return []; } // Sort versions by timestamp const sortedVersions = [...versions].sort((a, b) => { const tsA = this.extractTimestamp(a); const tsB = this.extractTimestamp(b); return tsA - tsB; }); const fromIndex = sortedVersions.indexOf(from); const toIndex = sortedVersions.indexOf(to); if (fromIndex === -1 || toIndex === -1) { return null; } // Build linear chain const chain = []; if (fromIndex < toIndex) { // Forward: from → to for (let i = fromIndex; i < toIndex; i++) { chain.push(sortedVersions[i]); } chain.push(to); } else { // Backward: from → to (going back in time) for (let i = fromIndex; i > toIndex; i--) { chain.push(sortedVersions[i]); } chain.push(to); } // Check chain length if (chain.length - 1 > this.maxChainLength) { throw new Error(`Transformation chain too long: ${chain.length - 1} hops (max: ${this.maxChainLength})`); } return chain; } /** * Execute transformation chain for request data */ async transformRequest(data, from, to, versions, options = {}) { return this.executeChain(data, from, to, versions, 'request', options); } /** * Execute transformation chain for response data */ async transformResponse(data, from, to, versions, options = {}) { return this.executeChain(data, from, to, versions, 'response', options); } /** * Execute transformation chain */ async executeChain(data, from, to, versions, type, options) { const chain = this.buildChain(from, to, versions); if (!chain) { return { success: false, error: new Error(`Cannot build transformation chain from ${from} to ${to}`), transformedVersions: [], chainLength: 0, }; } if (chain.length === 0) { // No transformation needed return { success: true, data, transformedVersions: [], chainLength: 0, }; } let currentData = data; const transformedVersions = []; try { // Execute chain for (let i = 0; i < chain.length - 1; i++) { const fromVer = chain[i]; const toVer = chain[i + 1]; const transformer = this.getTransformer(fromVer, toVer); if (!transformer) { throw new Error(`No transformer found between ${fromVer} and ${toVer}`); } // Determine direction const isForward = this.extractTimestamp(fromVer) < this.extractTimestamp(toVer); const direction = isForward ? transformer.forward : transformer.backward; // Get transform function const transformFn = type === 'request' ? direction.transformRequest : direction.transformResponse; if (transformFn) { // Apply transformation with timeout const result = transformFn(currentData); if (options.timeout) { currentData = await this.withTimeout(Promise.resolve(result), options.timeout); } else { currentData = await Promise.resolve(result); } } transformedVersions.push(toVer); } return { success: true, data: currentData, transformedVersions, chainLength: chain.length - 1, }; } catch (error) { if (options.fallbackOnError) { // Return original data on error return { success: false, data, error: error, transformedVersions, }; } return { success: false, error: error, transformedVersions, }; } } /** * Get all registered transformers */ getAllTransformers() { const seen = new Set(); const transformers = []; for (const [key, transformer] of this.transformers.entries()) { const uniqueKey = `${transformer.fromVersion}-${transformer.toVersion}`; if (!seen.has(uniqueKey)) { seen.add(uniqueKey); transformers.push(transformer); } } return transformers; } /** * Get transformer count */ getTransformerCount() { return this.getAllTransformers().length; } /** * Clear all transformers (for testing) */ clear() { this.transformers.clear(); } /** * Create key for transformer lookup */ makeKey(from, to) { return `${from}${to}`; } /** * Extract timestamp from TSV */ extractTimestamp(tsv) { const match = tsv.match(/^tsv:(\d+)-/); return match ? parseInt(match[1], 10) : 0; } /** * Execute promise with timeout */ async withTimeout(promise, timeoutMs) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('Transformation timeout')), timeoutMs)), ]); } } /** * Create a transformer pair */ export function createTransformerPair(fromVersion, toVersion, forward, backward, createdBy = 'system') { return { fromVersion, toVersion, immutable: true, createdAt: Date.now(), createdBy, forward, backward, }; } //# sourceMappingURL=transformer.js.map