@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
235 lines • 7.84 kB
JavaScript
/**
* 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