UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

491 lines 16 kB
export class VersionRegistry { state = { modules: {}, schemas: {}, config: null, routes: {}, events: {}, effects: {}, handlers: {}, tags: {}, activeVersions: new Set(), coldVersions: new Set(), }; history = new Map(); versionCache = new Map(); maxCacheSize = 1000; classificationConfig = { hotThresholdRequests: 100, warmThresholdRequests: 10, coldThresholdMs: 7 * 24 * 60 * 60 * 1000, // 7 days classificationWindowMs: 24 * 60 * 60 * 1000, // 24 hours }; constructor(initialState, config) { if (initialState) { this.state = { ...this.state, ...initialState, activeVersions: new Set(initialState.activeVersions || []), coldVersions: new Set(initialState.coldVersions || []), }; } if (config) { this.classificationConfig = { ...this.classificationConfig, ...config }; } } register(artifact) { const { type, id, version } = artifact; switch (type) { case 'module': this.state.modules[id] = version; break; case 'schema': this.state.schemas[id] = version; break; case 'config': this.state.config = version; break; case 'route': this.state.routes[id] = version; break; case 'event_handler': this.state.events[id] = version; break; case 'effect': this.state.effects[id] = version; break; default: // For other types, we might just log or ignore for the main registry state // depending on if they need global tracking break; } this.addToHistory(type, id, version); } get(type, id) { switch (type) { case 'module': return this.state.modules[id]; case 'schema': return this.state.schemas[id]; case 'route': return this.state.routes[id]; case 'event_handler': return this.state.events[id]; case 'effect': return this.state.effects[id]; } return undefined; } getConfigVersion() { return this.state.config; } getAll() { return { ...this.state }; } addToHistory(type, id, version) { const key = `${type}:${id}`; if (!this.history.has(key)) { this.history.set(key, []); } this.history.get(key)?.push(version); } getHistory(type, id) { return this.history.get(`${type}:${id}`) || []; } /** * Register a handler version in the timeline */ registerVersion(handlerPath, tsv, metadata) { if (!this.state.handlers[handlerPath]) { this.state.handlers[handlerPath] = { handlerPath, versions: [], }; } const timeline = this.state.handlers[handlerPath]; const timestamp = this.extractTimestamp(tsv); const versionInfo = { tsv, timestamp, hash: metadata.hash || '', status: metadata.status || 'hot', requestCount: metadata.requestCount || 0, lastAccessed: metadata.lastAccessed || Date.now(), tags: metadata.tags || [], dbSchemaVersion: metadata.dbSchemaVersion, }; // Insert in chronological order const insertIndex = timeline.versions.findIndex(v => v.timestamp > timestamp); if (insertIndex === -1) { timeline.versions.push(versionInfo); } else { timeline.versions.splice(insertIndex, 0, versionInfo); } this.state.activeVersions.add(tsv); this.clearCache(); } /** * Get version at a specific timestamp using binary search */ getVersionAt(handlerPath, timestamp) { const cacheKey = `${handlerPath}:${timestamp}`; if (this.versionCache.has(cacheKey)) { return this.versionCache.get(cacheKey); } const timeline = this.state.handlers[handlerPath]; if (!timeline || timeline.versions.length === 0) { return undefined; } // Binary search for version at or before timestamp let left = 0; let right = timeline.versions.length - 1; let result; while (left <= right) { const mid = Math.floor((left + right) / 2); const version = timeline.versions[mid]; if (version.timestamp <= timestamp) { result = version.tsv; left = mid + 1; } else { right = mid - 1; } } if (result) { this.cacheVersion(cacheKey, result); } return result; } /** * Get latest version for a handler */ getLatestVersion(handlerPath) { const timeline = this.state.handlers[handlerPath]; if (!timeline || timeline.versions.length === 0) { return undefined; } return timeline.versions[timeline.versions.length - 1].tsv; } /** * Tag a version with a semantic label */ tagVersion(tsv, label, createdBy = 'system') { const tag = { label, tsv, createdAt: Date.now(), createdBy, }; this.state.tags[label] = tag; // Add tag to version info for (const timeline of Object.values(this.state.handlers)) { const version = timeline.versions.find(v => v.tsv === tsv); if (version && !version.tags.includes(label)) { version.tags.push(label); } } this.clearCache(); } /** * Get version by semantic tag */ getVersionByTag(handlerPath, tag) { const versionTag = this.state.tags[tag]; if (!versionTag) { return undefined; } // Verify the tagged version exists for this handler const timeline = this.state.handlers[handlerPath]; if (!timeline) { return undefined; } const exists = timeline.versions.some(v => v.tsv === versionTag.tsv); return exists ? versionTag.tsv : undefined; } /** * Remove a tag */ untagVersion(label) { const tag = this.state.tags[label]; if (!tag) { return false; } delete this.state.tags[label]; // Remove tag from version info for (const timeline of Object.values(this.state.handlers)) { for (const version of timeline.versions) { const index = version.tags.indexOf(label); if (index !== -1) { version.tags.splice(index, 1); } } } this.clearCache(); return true; } /** * Get all versions for a handler */ getVersions(handlerPath) { const timeline = this.state.handlers[handlerPath]; return timeline ? [...timeline.versions] : []; } /** * Get all active versions */ getActiveVersions(handlerPath) { if (handlerPath) { const timeline = this.state.handlers[handlerPath]; if (!timeline) return []; return timeline.versions .filter(v => this.state.activeVersions.has(v.tsv)) .map(v => v.tsv); } return Array.from(this.state.activeVersions); } /** * Mark a version as cold */ markCold(tsv) { this.state.coldVersions.add(tsv); this.state.activeVersions.delete(tsv); // Update status in version info for (const timeline of Object.values(this.state.handlers)) { const version = timeline.versions.find(v => v.tsv === tsv); if (version) { version.status = 'cold'; } } this.clearCache(); } /** * Update version status */ updateVersionStatus(tsv, status) { for (const timeline of Object.values(this.state.handlers)) { const version = timeline.versions.find(v => v.tsv === tsv); if (version) { version.status = status; if (status === 'cold') { this.state.coldVersions.add(tsv); this.state.activeVersions.delete(tsv); } else { this.state.activeVersions.add(tsv); this.state.coldVersions.delete(tsv); } } } } /** * Increment request count for a version */ recordRequest(tsv) { for (const timeline of Object.values(this.state.handlers)) { const version = timeline.versions.find(v => v.tsv === tsv); if (version) { version.requestCount++; version.lastAccessed = Date.now(); // Reclassify based on new activity this.classifyVersion(version); } } } /** * Classify version as hot/warm/cold based on usage patterns */ classifyVersion(version) { const now = Date.now(); const timeSinceLastAccess = now - version.lastAccessed; // Check if cold (no recent access) if (timeSinceLastAccess > this.classificationConfig.coldThresholdMs) { if (version.status !== 'cold') { version.status = 'cold'; this.state.coldVersions.add(version.tsv); this.state.activeVersions.delete(version.tsv); } return; } // For hot/warm classification, we need to estimate requests in the window // Since we don't track request timestamps, we use a simple heuristic: // If requestCount is high and recently accessed, it's likely hot const estimatedRecentRequests = this.estimateRecentRequests(version, now); if (estimatedRecentRequests >= this.classificationConfig.hotThresholdRequests) { if (version.status !== 'hot') { version.status = 'hot'; this.state.activeVersions.add(version.tsv); this.state.coldVersions.delete(version.tsv); } } else if (estimatedRecentRequests >= this.classificationConfig.warmThresholdRequests) { if (version.status !== 'warm') { version.status = 'warm'; this.state.activeVersions.add(version.tsv); this.state.coldVersions.delete(version.tsv); } } else { // Low activity but not cold yet if (version.status !== 'warm') { version.status = 'warm'; this.state.activeVersions.add(version.tsv); this.state.coldVersions.delete(version.tsv); } } } /** * Estimate recent requests based on total count and last access time * This is a heuristic since we don't track individual request timestamps */ estimateRecentRequests(version, now) { const timeSinceLastAccess = now - version.lastAccessed; const windowMs = this.classificationConfig.classificationWindowMs; // If last access was within the window, assume requests are recent if (timeSinceLastAccess < windowMs) { // Simple decay: more recent = higher weight const recencyFactor = 1 - (timeSinceLastAccess / windowMs); return Math.floor(version.requestCount * recencyFactor); } return 0; } /** * Reclassify all versions (useful for periodic background jobs) */ reclassifyAllVersions() { for (const timeline of Object.values(this.state.handlers)) { for (const version of timeline.versions) { this.classifyVersion(version); } } } /** * Get versions by status */ getVersionsByStatus(status, handlerPath) { const results = []; if (handlerPath) { const timeline = this.state.handlers[handlerPath]; if (timeline) { results.push(...timeline.versions.filter(v => v.status === status)); } } else { for (const timeline of Object.values(this.state.handlers)) { results.push(...timeline.versions.filter(v => v.status === status)); } } return results; } /** * Get usage statistics */ getUsageStats(handlerPath) { let hot = 0; let warm = 0; let cold = 0; let totalRequests = 0; let totalVersions = 0; const timelines = handlerPath ? [this.state.handlers[handlerPath]].filter(Boolean) : Object.values(this.state.handlers); for (const timeline of timelines) { for (const version of timeline.versions) { totalVersions++; totalRequests += version.requestCount; switch (version.status) { case 'hot': hot++; break; case 'warm': warm++; break; case 'cold': cold++; break; } } } return { hot, warm, cold, totalRequests, totalVersions }; } /** * Update classification configuration */ updateClassificationConfig(config) { this.classificationConfig = { ...this.classificationConfig, ...config }; this.reclassifyAllVersions(); } /** * Get classification configuration */ getClassificationConfig() { return { ...this.classificationConfig }; } /** * Get version info */ getVersionInfo(tsv) { for (const timeline of Object.values(this.state.handlers)) { const version = timeline.versions.find(v => v.tsv === tsv); if (version) { return { ...version }; } } return undefined; } /** * Get all tags */ getAllTags() { return Object.values(this.state.tags); } /** * Get tags for a specific version */ getTagsForVersion(tsv) { const info = this.getVersionInfo(tsv); return info ? [...info.tags] : []; } /** * Extract timestamp from TSV */ extractTimestamp(tsv) { const match = tsv.match(/^tsv:(\d+)-/); return match ? parseInt(match[1], 10) : Date.now(); } /** * Cache a version lookup */ cacheVersion(key, tsv) { if (this.versionCache.size >= this.maxCacheSize) { // Simple LRU: remove first entry const firstKey = this.versionCache.keys().next().value; this.versionCache.delete(firstKey); } this.versionCache.set(key, tsv); } /** * Clear version cache */ clearCache() { this.versionCache.clear(); } /** * Serialize state for persistence */ serialize() { return JSON.stringify({ ...this.state, activeVersions: Array.from(this.state.activeVersions), coldVersions: Array.from(this.state.coldVersions), }); } /** * Deserialize state from persistence */ static deserialize(data) { const parsed = JSON.parse(data); return new VersionRegistry({ ...parsed, activeVersions: new Set(parsed.activeVersions || []), coldVersions: new Set(parsed.coldVersions || []), }); } } //# sourceMappingURL=registry.js.map