navskit
Version:
Deploy TypeScript logic on Ethereum. Includes core library, CLI tools, and utilities.
346 lines (337 loc) • 12.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.packageManager = exports.PackageManager = void 0;
/**
* Package management utilities for dynamic loading of NAVS packages
*/
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const child_process = __importStar(require("child_process"));
const util = __importStar(require("util"));
const crypto = __importStar(require("crypto"));
const logger_1 = require("./logger");
const exec = util.promisify(child_process.exec);
const mkdir = util.promisify(fs.mkdir);
const writeFile = util.promisify(fs.writeFile);
const access = util.promisify(fs.access);
const logger = (0, logger_1.createServiceLogger)('PackageManager', '0.0.1');
class PackageManager {
packageCache = new Map();
cacheDir;
constructor(cacheDir) {
this.cacheDir = cacheDir || path.join(process.cwd(), '.navs-cache');
}
/**
* Gets the cache key for a package name and version
*/
getCacheKey(serviceName, serviceVersion) {
return `${serviceName}@${serviceVersion}`;
}
/**
* Gets the installation directory for a package
*/
getPackageDir(serviceName, serviceVersion) {
const hash = crypto.createHash('md5').update(`${serviceName}@${serviceVersion}`).digest('hex');
return path.join(this.cacheDir, `pkg-${hash}`);
}
/**
* Checks if a package is already cached
*/
async isPackageCached(serviceName, serviceVersion) {
const cacheKey = this.getCacheKey(serviceName, serviceVersion);
if (this.packageCache.has(cacheKey)) {
return true;
}
const packageDir = this.getPackageDir(serviceName, serviceVersion);
try {
await access(path.join(packageDir, 'package.json'));
return true;
}
catch {
return false;
}
}
/**
* Downloads and installs a package to the cache
*/
async downloadPackage(serviceName, serviceVersion) {
const packageDir = this.getPackageDir(serviceName, serviceVersion);
logger.info(`Downloading package ${serviceName}@${serviceVersion} to ${packageDir}`);
// Create the package directory
await mkdir(packageDir, { recursive: true });
// Create a package.json for the isolated environment
const packageJson = {
name: `navs-isolated-${serviceName}`,
version: '1.0.0',
private: true,
type: 'module',
dependencies: {
[serviceName]: serviceVersion,
'navs-core': 'latest',
'viem': 'latest',
'reflect-metadata': 'latest'
}
};
await writeFile(path.join(packageDir, 'package.json'), JSON.stringify(packageJson, null, 2));
// Install the package
logger.info(`Installing ${serviceName}@${serviceVersion}...`);
try {
await exec('npm install', { cwd: packageDir });
logger.info(`Successfully installed ${serviceName}@${serviceVersion}`);
return packageDir;
}
catch (error) {
logger.error(`Failed to install package ${serviceName}@${serviceVersion}:`, error);
throw new Error(`Package installation failed: ${error.message}`);
}
}
/**
* Creates a worker script for executing functions in the package
*/
createWorkerScript(serviceName) {
return `
import { privateKeyToAccount } from 'viem/accounts';
import { navs, navsRegistry } from 'navs-core';
// Import the package - this will trigger all @navs decorators
import '${serviceName}';
// Listen for messages from parent process
process.on('message', async (message) => {
const { id, type, data } = message;
try {
if (type === 'listFunctions') {
// Return all functions registered for this service
const serviceFunctions = {};
for (const [name, funcInfo] of Object.entries(navsRegistry)) {
if (funcInfo.serviceName === '${serviceName}') {
serviceFunctions[name] = {
paramTypes: funcInfo.paramTypes,
returnType: funcInfo.returnType,
hasConsensus: !!funcInfo.consensus
};
}
}
process.send({
id,
success: true,
result: serviceFunctions
});
} else if (type === 'executeFunction') {
const { functionName, encodedArgs, isConsensus, consensusArgs } = data;
const funcInfo = navsRegistry[functionName];
if (!funcInfo) {
throw new Error(\`Function \${functionName} not found\`);
}
let result;
if (isConsensus && funcInfo.consensus) {
result = await funcInfo.consensus(consensusArgs);
} else {
// Decode the arguments using the function's decoder
const decodedArgs = funcInfo.decodeArgs(encodedArgs);
result = await funcInfo.fn(...decodedArgs);
}
process.send({
id,
success: true,
result
});
} else {
throw new Error(\`Unknown message type: \${type}\`);
}
} catch (error) {
process.send({
id,
success: false,
error: error.message,
stack: error.stack
});
}
});
// Send ready signal
process.send({ type: 'ready' });
`;
}
/**
* Starts a worker process for a package
*/
async startWorker(serviceName, serviceVersion) {
const packageDir = this.getPackageDir(serviceName, serviceVersion);
// Create worker script
const workerScript = this.createWorkerScript(serviceName);
const workerPath = path.join(packageDir, 'worker.mjs');
await writeFile(workerPath, workerScript);
// Start the worker process
const workerProcess = child_process.fork(workerPath, [], {
cwd: packageDir,
silent: false,
execArgv: ['--experimental-modules']
});
// Wait for ready signal
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker process failed to start within timeout'));
}, 30000);
workerProcess.on('message', (message) => {
if (message.type === 'ready') {
clearTimeout(timeout);
resolve();
}
});
workerProcess.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
workerProcess.on('exit', (code) => {
if (code !== 0) {
clearTimeout(timeout);
reject(new Error(`Worker process exited with code ${code}`));
}
});
});
logger.info(`Started worker process for ${serviceName}@${serviceVersion}`);
return workerProcess;
}
/**
* Sends a message to a worker process and waits for response
*/
async sendWorkerMessage(worker, type, data) {
const id = Math.random().toString(36);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker message timeout'));
}, 30000);
const messageHandler = (message) => {
if (message.id === id) {
clearTimeout(timeout);
worker.off('message', messageHandler);
if (message.success) {
resolve(message.result);
}
else {
reject(new Error(message.error));
}
}
};
worker.on('message', messageHandler);
worker.send({ id, type, data });
});
}
/**
* Gets a package, downloading it if necessary and starting worker if needed
*/
async getPackage(serviceName, serviceVersion) {
const cacheKey = this.getCacheKey(serviceName, serviceVersion);
// Check memory cache first
if (this.packageCache.has(cacheKey)) {
return this.packageCache.get(cacheKey);
}
// Check if package is cached on disk
if (!(await this.isPackageCached(serviceName, serviceVersion))) {
await this.downloadPackage(serviceName, serviceVersion);
}
// Start worker process
const worker = await this.startWorker(serviceName, serviceVersion);
const packageInfo = {
name: serviceName,
version: serviceVersion,
installPath: this.getPackageDir(serviceName, serviceVersion),
process: worker
};
// Cache the package info
this.packageCache.set(cacheKey, packageInfo);
return packageInfo;
}
/**
* Lists functions available in a package
*/
async listFunctions(serviceName, serviceVersion) {
const packageInfo = await this.getPackage(serviceName, serviceVersion);
if (!packageInfo.process) {
throw new Error('Package worker process not available');
}
return await this.sendWorkerMessage(packageInfo.process, 'listFunctions');
}
/**
* Executes a function in a package
*/
async executeFunction(serviceName, serviceVersion, functionName, encodedArgs, isConsensus = false, consensusArgs) {
const packageInfo = await this.getPackage(serviceName, serviceVersion);
if (!packageInfo.process) {
throw new Error('Package worker process not available');
}
return await this.sendWorkerMessage(packageInfo.process, 'executeFunction', {
functionName,
encodedArgs,
isConsensus,
consensusArgs
});
}
/**
* Cleans up resources for a package
*/
async cleanup(serviceName, serviceVersion) {
const cacheKey = this.getCacheKey(serviceName, serviceVersion);
const packageInfo = this.packageCache.get(cacheKey);
if (packageInfo?.process) {
packageInfo.process.kill();
}
this.packageCache.delete(cacheKey);
}
/**
* Clears all package cache and kills all worker processes
*/
async clearCache() {
for (const packageInfo of this.packageCache.values()) {
if (packageInfo.process) {
packageInfo.process.kill();
}
}
this.packageCache.clear();
}
/**
* Gets cache statistics
*/
getCacheStats() {
const activeWorkers = Array.from(this.packageCache.values())
.filter(pkg => pkg.process && !pkg.process.killed).length;
return {
cachedPackages: this.packageCache.size,
activeWorkers
};
}
}
exports.PackageManager = PackageManager;
// Export a default instance
exports.packageManager = new PackageManager();