UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

535 lines 16.9 kB
/** * @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