@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
535 lines • 16.9 kB
JavaScript
/**
* @module runtime/enhanced-route-manager
* @description Enhanced Route Manager with version resolution and Timescape integration
*
* This implements Task 9 from the runtime architecture spec:
* - Version resolution using Timescape
* - Manifest, GType, and health status caching
* - Handler instance selection and routing
* - Policy enforcement (rate limiting, authentication)
* - Warm pool management
* - Usage tracking for auto-decommissioning
*/
import { VersionRegistry } from './timescape/registry.js';
import { VersionResolver } from './timescape/resolver.js';
import { TransformerEngine } from './timescape/transformer.js';
/**
* Enhanced Route Manager with version resolution and Timescape integration
*/
export class EnhancedRouteManager {
registry;
resolver;
transformerEngine;
// Handler instances by path and version
instances = new Map();
// Manifest cache
manifestCache = new Map();
// GType cache
gtypeCache = new Map();
// Health status cache
healthCache = new Map();
// Rate limiting state
rateLimitState = new Map();
// Warm pool configurations
warmPools = new Map();
// Usage tracking
usageMetrics = new Map();
// Cache configuration
maxCacheSize = 1000;
healthCheckInterval = 30000; // 30 seconds
rateLimitCleanupInterval = 60000; // 1 minute
constructor(registry, transformerEngine) {
this.registry = registry || new VersionRegistry();
this.resolver = new VersionResolver(this.registry);
this.transformerEngine = transformerEngine || new TransformerEngine();
// Start background tasks
this.startHealthCheckLoop();
this.startRateLimitCleanup();
}
/**
* Register a handler instance
*/
registerHandler(path, version, handler, manifest) {
if (!this.instances.has(path)) {
this.instances.set(path, new Map());
}
const instance = {
id: `${path}:${version}`,
handlerId: manifest.handlerId,
version,
handler,
manifest,
health: {
status: 'healthy',
lastCheck: Date.now(),
consecutiveFailures: 0,
},
createdAt: Date.now(),
lastAccessed: Date.now(),
};
this.instances.get(path).set(version, instance);
// Cache manifest
this.cacheManifest(manifest);
// Register version in Timescape
this.registry.registerVersion(path, version, {
hash: manifest.hash,
status: 'hot',
requestCount: 0,
lastAccessed: Date.now(),
tags: [],
});
}
/**
* Resolve handler version using Timescape
*/
resolveVersion(path, query = {}, headers = {}) {
const result = this.resolver.resolveVersion(path, query, headers);
if ('code' in result) {
return {
code: 'NO_VERSION',
message: result.message,
details: result.details,
};
}
return result.version;
}
/**
* Route a request to the appropriate handler instance
*/
async routeRequest(descriptor) {
// 1. Resolve version
const versionResult = this.resolveVersion(descriptor.path, descriptor.query, descriptor.headers);
if (typeof versionResult !== 'string') {
return versionResult;
}
const version = versionResult;
// 2. Get handler instance
const instance = this.getInstance(descriptor.path, version);
if (!instance) {
return {
code: 'NO_HANDLER',
message: `No handler instance found for ${descriptor.path} version ${version}`,
};
}
// 3. Check health
if (instance.health.status === 'unhealthy') {
return {
code: 'UNHEALTHY',
message: `Handler instance is unhealthy: ${instance.health.message || 'unknown reason'}`,
};
}
// 4. Enforce rate limiting
const rateLimitError = this.enforceRateLimit(instance.manifest, descriptor.clientId);
if (rateLimitError) {
return rateLimitError;
}
// 5. Verify authentication
const authError = this.verifyAuthentication(instance.manifest, descriptor.authContext);
if (authError) {
return authError;
}
// 6. Check if request needs transformation
const requestedVersion = this.extractRequestedVersion(descriptor.headers);
let transformedRequest;
let requiresResponseTransform = false;
let originalVersion;
if (requestedVersion && requestedVersion !== version) {
// Request is for an old version, but we're routing to a new version
// Transform the request from old version to new version
const transformResult = await this.transformRequest(descriptor.body, requestedVersion, version, descriptor.path);
if (!transformResult.success) {
return {
code: 'NO_VERSION',
message: `Failed to transform request from ${requestedVersion} to ${version}: ${transformResult.error?.message}`,
details: transformResult,
};
}
transformedRequest = transformResult;
requiresResponseTransform = true;
originalVersion = requestedVersion;
}
// 7. Track usage
this.trackUsage(instance.id, {
requestCount: 1,
errorCount: 0,
avgLatency: 0,
lastAccessed: Date.now(),
});
// 8. Update instance last accessed
instance.lastAccessed = Date.now();
// 9. Record request in Timescape
this.registry.recordRequest(version);
return {
instance,
manifest: instance.manifest,
version,
cached: this.manifestCache.has(instance.manifest.handlerId),
transformedRequest,
requiresResponseTransform,
originalVersion,
};
}
/**
* Enforce rate limiting policies
*/
enforceRateLimit(manifest, clientId) {
if (!manifest.policies?.rateLimit) {
return null;
}
const { limit, window } = manifest.policies.rateLimit;
const key = `${manifest.handlerId}:${clientId}`;
const now = Date.now();
let state = this.rateLimitState.get(key);
// Initialize or reset window
if (!state || now - state.windowStart >= window) {
state = {
count: 0,
windowStart: now,
};
this.rateLimitState.set(key, state);
}
// Check limit
if (state.count >= limit) {
return {
code: 'RATE_LIMITED',
message: `Rate limit exceeded: ${limit} requests per ${window}ms`,
details: {
limit,
window,
current: state.count,
},
};
}
// Increment count
state.count++;
return null;
}
/**
* Verify authentication requirements
*/
verifyAuthentication(manifest, authContext) {
if (!manifest.policies?.roles || manifest.policies.roles.length === 0) {
return null;
}
if (!authContext) {
return {
code: 'UNAUTHORIZED',
message: 'Authentication required',
details: {
requiredRoles: manifest.policies.roles,
},
};
}
const hasRequiredRole = manifest.policies.roles.some(role => authContext.roles.includes(role));
if (!hasRequiredRole) {
return {
code: 'UNAUTHORIZED',
message: 'Insufficient permissions',
details: {
requiredRoles: manifest.policies.roles,
userRoles: authContext.roles,
},
};
}
return null;
}
/**
* Get handler instance
*/
getInstance(path, version) {
return this.instances.get(path)?.get(version);
}
/**
* Cache manifest
*/
cacheManifest(manifest) {
if (this.manifestCache.size >= this.maxCacheSize) {
const firstKey = this.manifestCache.keys().next().value;
if (firstKey) {
this.manifestCache.delete(firstKey);
}
}
this.manifestCache.set(manifest.handlerId, manifest);
}
/**
* Get cached manifest
*/
getManifest(handlerId) {
return this.manifestCache.get(handlerId);
}
/**
* Cache GType schema
*/
cacheGType(ref, schema) {
if (this.gtypeCache.size >= this.maxCacheSize) {
const firstKey = this.gtypeCache.keys().next().value;
if (firstKey) {
this.gtypeCache.delete(firstKey);
}
}
this.gtypeCache.set(ref, schema);
}
/**
* Get cached GType schema
*/
getGType(ref) {
return this.gtypeCache.get(ref);
}
/**
* Maintain warm pool for critical versions
*/
maintainWarmPool(handlerId, config) {
this.warmPools.set(handlerId, config);
}
/**
* Get warm pool configuration
*/
getWarmPoolConfig(handlerId) {
return this.warmPools.get(handlerId);
}
/**
* Track usage metrics
*/
trackUsage(instanceId, metrics) {
const existing = this.usageMetrics.get(instanceId) || {
requestCount: 0,
errorCount: 0,
avgLatency: 0,
lastAccessed: Date.now(),
};
this.usageMetrics.set(instanceId, {
requestCount: existing.requestCount + (metrics.requestCount || 0),
errorCount: existing.errorCount + (metrics.errorCount || 0),
avgLatency: metrics.avgLatency !== undefined ? metrics.avgLatency : existing.avgLatency,
lastAccessed: metrics.lastAccessed || existing.lastAccessed,
});
}
/**
* Get usage metrics
*/
getUsageMetrics(instanceId) {
return this.usageMetrics.get(instanceId);
}
/**
* Get all instances for a path
*/
getInstances(path) {
const instances = this.instances.get(path);
return instances ? Array.from(instances.values()) : [];
}
/**
* Get all registered paths
*/
getPaths() {
return Array.from(this.instances.keys());
}
/**
* Update health status
*/
updateHealth(path, version, health) {
const instance = this.getInstance(path, version);
if (instance) {
instance.health = health;
this.healthCache.set(instance.id, health);
}
}
/**
* Get health status
*/
getHealth(path, version) {
const instance = this.getInstance(path, version);
return instance?.health;
}
/**
* Start health check loop
*/
startHealthCheckLoop() {
setInterval(() => {
this.performHealthChecks();
}, this.healthCheckInterval);
}
/**
* Perform health checks on all instances
*/
performHealthChecks() {
for (const [path, versionMap] of this.instances) {
for (const [version, instance] of versionMap) {
// Simple health check: mark as degraded if not accessed recently
const timeSinceAccess = Date.now() - instance.lastAccessed;
const fiveMinutes = 5 * 60 * 1000;
if (timeSinceAccess > fiveMinutes && instance.health.status === 'healthy') {
instance.health.status = 'degraded';
instance.health.lastCheck = Date.now();
}
}
}
}
/**
* Start rate limit cleanup
*/
startRateLimitCleanup() {
setInterval(() => {
this.cleanupRateLimits();
}, this.rateLimitCleanupInterval);
}
/**
* Clean up expired rate limit entries
*/
cleanupRateLimits() {
const now = Date.now();
const maxWindow = 60000; // 1 minute max window
for (const [key, state] of this.rateLimitState) {
if (now - state.windowStart > maxWindow) {
this.rateLimitState.delete(key);
}
}
}
/**
* Get registry for external access
*/
getRegistry() {
return this.registry;
}
/**
* Get resolver for external access
*/
getResolver() {
return this.resolver;
}
/**
* Clear all caches
*/
clearCaches() {
this.manifestCache.clear();
this.gtypeCache.clear();
this.healthCache.clear();
this.resolver.clearCache();
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
manifests: this.manifestCache.size,
gtypes: this.gtypeCache.size,
health: this.healthCache.size,
rateLimits: this.rateLimitState.size,
};
}
/**
* Register a transformer pair
*/
registerTransformer(transformer) {
this.transformerEngine.register(transformer);
}
/**
* Get transformer between two versions
*/
getTransformer(from, to) {
return this.transformerEngine.getTransformer(from, to);
}
/**
* Check if transformer exists
*/
hasTransformer(from, to) {
return this.transformerEngine.hasTransformer(from, to);
}
/**
* Transform request data from one version to another
*/
async transformRequest(data, fromVersion, toVersion, path) {
// Get all versions for this path
const versions = this.getVersionsForPath(path);
if (versions.length === 0) {
return {
success: false,
error: new Error(`No versions found for path ${path}`),
transformedVersions: [],
chainLength: 0,
};
}
// Execute transformation chain
return this.transformerEngine.transformRequest(data, fromVersion, toVersion, versions, {
maxHops: 10,
timeout: 5000,
fallbackOnError: false,
});
}
/**
* Transform response data from one version to another
*/
async transformResponse(data, fromVersion, toVersion, path) {
// Get all versions for this path
const versions = this.getVersionsForPath(path);
if (versions.length === 0) {
return {
success: false,
error: new Error(`No versions found for path ${path}`),
transformedVersions: [],
chainLength: 0,
};
}
// Execute transformation chain
return this.transformerEngine.transformResponse(data, fromVersion, toVersion, versions, {
maxHops: 10,
timeout: 5000,
fallbackOnError: false,
});
}
/**
* Get all versions for a path (sorted by timestamp)
*/
getVersionsForPath(path) {
const versionMap = this.instances.get(path);
if (!versionMap) {
return [];
}
return Array.from(versionMap.keys()).sort((a, b) => {
const tsA = this.extractTimestamp(a);
const tsB = this.extractTimestamp(b);
return tsA - tsB;
});
}
/**
* Extract timestamp from TSV
*/
extractTimestamp(tsv) {
const match = tsv.match(/^tsv:(\d+)-/);
return match ? parseInt(match[1], 10) : 0;
}
/**
* Extract requested version from headers
*/
extractRequestedVersion(headers) {
const versionHeader = headers['x-gati-version'] || headers['X-Gati-Version'];
if (typeof versionHeader === 'string' && versionHeader.startsWith('tsv:')) {
return versionHeader;
}
return undefined;
}
/**
* Get transformer engine for external access
*/
getTransformerEngine() {
return this.transformerEngine;
}
/**
* Get all registered transformers
*/
getAllTransformers() {
return this.transformerEngine.getAllTransformers();
}
/**
* Get transformer count
*/
getTransformerCount() {
return this.transformerEngine.getTransformerCount();
}
}
/**
* Create an enhanced route manager instance
*/
export function createEnhancedRouteManager(registry, transformerEngine) {
return new EnhancedRouteManager(registry, transformerEngine);
}
//# sourceMappingURL=enhanced-route-manager.js.map