UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

354 lines 11.8 kB
/** * @module runtime/module-rpc * @description Module RPC adapters with automatic serialization, retry logic, and connection pooling */ import { logger } from './logger.js'; /** * RPC error types */ export class RPCError extends Error { moduleName; method; cause; constructor(message, moduleName, method, cause) { super(message); this.moduleName = moduleName; this.method = method; this.cause = cause; this.name = 'RPCError'; if (Error.captureStackTrace) { Error.captureStackTrace(this, RPCError); } } } export class RPCTimeoutError extends RPCError { constructor(moduleName, method, timeout) { super(`RPC call timed out after ${timeout}ms: ${moduleName}.${method}`, moduleName, method); this.name = 'RPCTimeoutError'; } } export class RPCSerializationError extends RPCError { constructor(moduleName, method, cause) { super(`Failed to serialize/deserialize RPC call: ${moduleName}.${method}`, moduleName, method, cause); this.name = 'RPCSerializationError'; } } /** * Connection pool for module RPC calls */ export class ConnectionPool { connections = new Map(); config; constructor(config = {}) { this.config = { maxConnections: config.maxConnections ?? 10, minConnections: config.minConnections ?? 1, idleTimeout: config.idleTimeout ?? 60000, connectionTimeout: config.connectionTimeout ?? 5000, }; } /** * Get or create a connection for a module */ async acquire(moduleName) { const pool = this.connections.get(moduleName) || []; // Find an available connection const available = pool.find((conn) => !conn.inUse && !conn.closed); if (available) { available.inUse = true; available.lastUsed = Date.now(); return available; } // Create new connection if under max limit if (pool.length < this.config.maxConnections) { const conn = this.createConnection(moduleName); pool.push(conn); this.connections.set(moduleName, pool); return conn; } // Wait for a connection to become available return this.waitForConnection(moduleName); } /** * Release a connection back to the pool */ release(connection) { connection.inUse = false; connection.lastUsed = Date.now(); } /** * Create a new connection */ createConnection(moduleName) { const conn = { id: `${moduleName}-${Date.now()}-${Math.random().toString(36).slice(2)}`, moduleName, inUse: true, closed: false, createdAt: Date.now(), lastUsed: Date.now(), }; return conn; } /** * Wait for a connection to become available */ async waitForConnection(moduleName) { const startTime = Date.now(); while (Date.now() - startTime < this.config.connectionTimeout) { const pool = this.connections.get(moduleName) || []; const available = pool.find((conn) => !conn.inUse && !conn.closed); if (available) { available.inUse = true; available.lastUsed = Date.now(); return available; } // Wait a bit before checking again await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error(`Connection timeout: no available connections for ${moduleName}`); } /** * Close idle connections */ closeIdleConnections() { const now = Date.now(); for (const [moduleName, pool] of this.connections.entries()) { const activeConnections = pool.filter((conn) => { if (conn.closed) return false; if (conn.inUse) return true; const idle = now - conn.lastUsed; if (idle > this.config.idleTimeout) { conn.closed = true; return false; } return true; }); if (activeConnections.length === 0) { this.connections.delete(moduleName); } else { this.connections.set(moduleName, activeConnections); } } } /** * Close all connections for a module */ closeModule(moduleName) { const pool = this.connections.get(moduleName); if (pool) { pool.forEach((conn) => { conn.closed = true; }); this.connections.delete(moduleName); } } /** * Close all connections */ closeAll() { for (const pool of this.connections.values()) { pool.forEach((conn) => { conn.closed = true; }); } this.connections.clear(); } /** * Get pool statistics */ getStatistics() { const stats = new Map(); for (const [moduleName, pool] of this.connections.entries()) { stats.set(moduleName, { total: pool.length, inUse: pool.filter((c) => c.inUse).length, idle: pool.filter((c) => !c.inUse && !c.closed).length, closed: pool.filter((c) => c.closed).length, }); } return stats; } } /** * Module RPC client with automatic serialization, retry logic, and connection pooling */ export class ModuleRPCClient { moduleName; moduleExports; pool; defaultOptions; constructor(moduleName, moduleExports, pool, options) { this.moduleName = moduleName; this.moduleExports = moduleExports; this.pool = pool || new ConnectionPool(); this.defaultOptions = { timeout: options?.timeout ?? 5000, maxRetries: options?.maxRetries ?? 3, retryDelay: options?.retryDelay ?? 100, backoffMultiplier: options?.backoffMultiplier ?? 2, maxRetryDelay: options?.maxRetryDelay ?? 5000, retryOnTimeout: options?.retryOnTimeout ?? true, }; } /** * Call a module method with automatic serialization, retry, and timeout */ async call(method, args = [], options) { const opts = { ...this.defaultOptions, ...options }; let lastError; for (let attempt = 0; attempt <= opts.maxRetries; attempt++) { try { return await this.executeCall(method, args, opts); } catch (error) { lastError = error; // Don't retry on certain errors if (error instanceof RPCSerializationError) { throw error; } // Don't retry on timeout if retryOnTimeout is false if (error instanceof RPCTimeoutError && !opts.retryOnTimeout) { throw error; } // If this is the last attempt, throw the original error if (attempt >= opts.maxRetries) { throw lastError; } // Calculate retry delay with exponential backoff const delay = Math.min(opts.retryDelay * Math.pow(opts.backoffMultiplier, attempt), opts.maxRetryDelay); logger.warn({ module: this.moduleName, method, attempt: attempt + 1, maxRetries: opts.maxRetries, delay, error: error instanceof Error ? error.message : String(error), }, 'RPC call failed, retrying'); await new Promise((resolve) => setTimeout(resolve, delay)); } } // This should never be reached, but TypeScript needs it throw new RPCError(`RPC call failed after ${opts.maxRetries + 1} attempts`, this.moduleName, method, lastError); } /** * Execute a single RPC call */ async executeCall(method, args, options) { // Acquire connection from pool const connection = await this.pool.acquire(this.moduleName); try { // Serialize arguments let serializedArgs; try { serializedArgs = this.serialize(args); } catch (error) { throw new RPCSerializationError(this.moduleName, method, error); } // Get the method from module exports const moduleMethod = this.moduleExports[method]; if (typeof moduleMethod !== 'function') { throw new RPCError(`Method not found: ${method}`, this.moduleName, method); } // Execute with timeout const result = await this.withTimeout(moduleMethod.apply(this.moduleExports, serializedArgs), options.timeout, method); // Deserialize result try { return this.deserialize(result); } catch (error) { throw new RPCSerializationError(this.moduleName, method, error); } } finally { // Release connection back to pool this.pool.release(connection); } } /** * Execute a promise with timeout */ async withTimeout(promise, timeout, method) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new RPCTimeoutError(this.moduleName, method, timeout)); }, timeout); }); return Promise.race([promise, timeoutPromise]); } /** * Serialize arguments for RPC call */ serialize(args) { // For now, we use JSON serialization // In the future, this could support other serialization formats return JSON.parse(JSON.stringify(args)); } /** * Deserialize result from RPC call */ deserialize(result) { // For now, we use JSON serialization // In the future, this could support other serialization formats return JSON.parse(JSON.stringify(result)); } /** * Create a typed proxy for the module */ createProxy() { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return return new Proxy({}, { get: (_, prop) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args) => this.call(prop, args); }, }); } } /** * Create a module RPC client */ export function createModuleRPCClient(moduleName, moduleExports, pool, options) { return new ModuleRPCClient(moduleName, moduleExports, pool, options); } /** * Create a typed module client proxy */ export function createModuleClient(moduleName, moduleExports, pool, options) { const client = createModuleRPCClient(moduleName, moduleExports, pool, options); return client.createProxy(); } /** * Global connection pool instance */ let globalPool; /** * Get or create the global connection pool */ export function getGlobalConnectionPool() { if (!globalPool) { globalPool = new ConnectionPool(); } return globalPool; } /** * Set the global connection pool */ export function setGlobalConnectionPool(pool) { globalPool = pool; } /** * Close the global connection pool */ export function closeGlobalConnectionPool() { if (globalPool) { globalPool.closeAll(); globalPool = undefined; } } //# sourceMappingURL=module-rpc.js.map