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