@iqai/mcp-near-agent
Version:
Mcp server for Near
362 lines โข 14.2 kB
JavaScript
import { EventEmitter } from "node:events";
import { AuthManager } from "./auth-manager.js";
import { EventListener } from "./event-listener.js";
import { EventProcessor } from "./event-processor.js";
import { subscriptionManager, } from "./subscription-manager.js";
export class EventWatcher extends EventEmitter {
static instance;
authManager;
eventListener;
eventProcessor;
server = null;
isInitialized = false;
startTime = Date.now();
// Statistics tracking
stats = {
totalEventsDetected: 0,
totalEventsProcessed: 0,
totalEventsSuccessful: 0,
totalEventsFailed: 0,
totalProcessingTime: 0,
};
constructor() {
super();
this.authManager = AuthManager.getInstance();
this.eventListener = new EventListener();
this.eventProcessor = new EventProcessor();
this.setupEventHandlers();
}
static getInstance() {
if (!EventWatcher.instance) {
EventWatcher.instance = new EventWatcher();
}
return EventWatcher.instance;
}
/**
* Set the MCP server instance for message creation
*/
setServer(server) {
this.server = server;
this.eventProcessor.setServer(this.server);
}
/**
* Initialize the EventWatcher - handles all authentication internally
*/
async initialize() {
if (this.isInitialized) {
return;
}
console.log("๐ Initializing EventWatcher...");
try {
// Initialize AuthManager if not already done
if (!this.authManager.isReady()) {
console.log("๐ Initializing NEAR account via AuthManager...");
await this.authManager.initialize();
}
// Validate connection
const isValid = await this.authManager.validateConnection();
if (!isValid) {
throw new Error("NEAR account connection is not valid");
}
const account = this.authManager.getAccount();
if (!account) {
throw new Error("Failed to get NEAR account from AuthManager");
}
// Initialize components
await this.eventListener.initialize();
this.eventProcessor.setAccount(account);
this.isInitialized = true;
console.log("โ
EventWatcher initialized successfully");
}
catch (error) {
console.error("โ Failed to initialize EventWatcher:", error);
throw new Error(`EventWatcher initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Start watching for a specific event
*/
async watchEvent(request) {
// Initialize if not already done
await this.initialize();
const { contractId, eventName, responseMethodName, responseParameterName, cronExpression, server, } = request;
// Check if already watching this event
if (subscriptionManager.hasSubscription(contractId, eventName)) {
throw new Error(`Already watching event '${eventName}' on contract '${contractId}'`);
}
try {
console.log(`๐ฏ Starting to watch event '${eventName}' on contract '${contractId}'`);
// Create subscription
const subscriptionConfig = {
contractId,
eventName,
responseMethodName,
responseParameterName,
cronExpression: cronExpression || "*/10 * * * * *",
server,
};
const subscription = subscriptionManager.subscribe(subscriptionConfig);
// Start listening with EventListener
const cronJob = this.eventListener.startListening(subscription);
// Update subscription with cron job reference
subscriptionManager.updateCronJob(contractId, eventName, cronJob);
this.emit("watcher:started", subscription.id);
this.emitStatsUpdate();
console.log(`โ
Successfully started watching event '${eventName}' on contract '${contractId}'`);
return subscription.id;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("โ Failed to start watching event:", error);
// Clean up partial subscription if it was created
subscriptionManager.unsubscribe(contractId, eventName);
throw new Error(`Failed to watch event: ${errorMessage}`);
}
}
/**
* Stop watching a specific event
*/
async stopWatching(contractId, eventName) {
const subscription = subscriptionManager.getSubscription(contractId, eventName);
if (!subscription) {
console.warn(`โ ๏ธ No active subscription found for event '${eventName}' on contract '${contractId}'`);
return false;
}
try {
console.log(`๐ Stopping watch for event '${eventName}' on contract '${contractId}'`);
// Stop listening
this.eventListener.stopListening(subscription.id);
// Remove subscription
const success = subscriptionManager.unsubscribe(contractId, eventName);
if (success) {
this.emit("watcher:stopped", subscription.id);
this.emitStatsUpdate();
console.log(`โ
Successfully stopped watching event '${eventName}' on contract '${contractId}'`);
}
return success;
}
catch (error) {
console.error("โ Error stopping watch for event:", error);
this.emit("watcher:error", { subscriptionId: subscription.id, error });
return false;
}
}
/**
* Stop watching all events
*/
async stopAllWatching() {
console.log("๐ Stopping all event watching...");
const activeSubscriptions = subscriptionManager.getActiveSubscriptions();
for (const subscription of activeSubscriptions) {
try {
await this.stopWatching(subscription.contractId, subscription.eventName);
}
catch (error) {
console.error(`โ Error stopping subscription ${subscription.id}:`, error);
}
}
console.log("โ
All event watching stopped");
}
/**
* Get current watching status
*/
getWatchingStatus() {
const subscriptions = subscriptionManager.getActiveSubscriptions();
const listenerStats = this.eventListener.getStats();
const processorStats = this.eventProcessor.getStats();
return {
isInitialized: this.isInitialized,
totalSubscriptions: subscriptions.length,
subscriptions: subscriptions.map((sub) => ({
id: sub.id,
contractId: sub.contractId,
eventName: sub.eventName,
responseMethodName: sub.responseMethodName,
responseParameterName: sub.responseParameterName,
cronExpression: sub.cronExpression,
isActive: sub.isActive,
createdAt: sub.createdAt,
lastEventAt: sub.lastEventAt,
})),
listener: listenerStats,
processor: processorStats,
};
}
/**
* Get comprehensive statistics
*/
getStats() {
const subscriptionStats = subscriptionManager.getStats();
return {
totalSubscriptions: subscriptionStats.total,
activeSubscriptions: subscriptionStats.active,
totalEventsDetected: this.stats.totalEventsDetected,
totalEventsProcessed: this.stats.totalEventsProcessed,
totalEventsSuccessful: this.stats.totalEventsSuccessful,
totalEventsFailed: this.stats.totalEventsFailed,
successRate: this.stats.totalEventsProcessed > 0
? (this.stats.totalEventsSuccessful /
this.stats.totalEventsProcessed) *
100
: 0,
averageProcessingTime: this.stats.totalEventsProcessed > 0
? this.stats.totalProcessingTime / this.stats.totalEventsProcessed
: 0,
uptime: Date.now() - this.startTime,
};
}
/**
* Pause watching for a specific event
*/
pauseWatching(contractId, eventName) {
return subscriptionManager.pauseSubscription(contractId, eventName);
}
/**
* Resume watching for a specific event
*/
resumeWatching(contractId, eventName) {
const subscription = subscriptionManager.getSubscription(contractId, eventName);
if (!subscription) {
return false;
}
const success = subscriptionManager.resumeSubscription(contractId, eventName);
if (success && subscription.cronJob) {
// Restart the cron job
subscription.cronJob.start();
}
return success;
}
/**
* Check if watching a specific event
*/
isWatching(contractId, eventName) {
return subscriptionManager.hasSubscription(contractId, eventName);
}
/**
* Get list of all watched events
*/
getWatchedEvents() {
return subscriptionManager.getActiveSubscriptions().map((sub) => ({
contractId: sub.contractId,
eventName: sub.eventName,
subscriptionId: sub.id,
}));
}
/**
* Setup event handlers to connect EventListener and EventProcessor
*/
setupEventHandlers() {
// Handle events found by EventListener
this.eventListener.on("event:found", async ({ event, subscription }) => {
console.log(`๐ Event detected: ${event.eventType} from ${subscription.contractId}`);
this.stats.totalEventsDetected++;
subscriptionManager.markEventReceived(subscription.contractId, subscription.eventName);
this.emit("event:detected", { event, subscription });
// Process the event
const account = this.authManager.getAccount();
if (account) {
try {
const result = await this.eventProcessor.processEvent({
subscription,
event,
account,
});
this.updateProcessingStats(result.success, result.processingTime);
if (result.success) {
this.emit("event:processed", {
requestId: result.requestId,
response: result.response || "No Response",
processingTime: result.processingTime,
});
}
else {
this.emit("event:failed", {
requestId: result.requestId,
error: result.error || "No error",
processingTime: result.processingTime,
});
}
}
catch (error) {
console.error("โ Unexpected error processing event:", error);
this.stats.totalEventsFailed++;
this.emit("event:failed", {
requestId: event.requestId,
error: error instanceof Error ? error.message : "Unknown error",
processingTime: 0,
});
}
}
this.emitStatsUpdate();
});
// Handle EventListener errors
this.eventListener.on("event:error", ({ subscription, error }) => {
console.error(`โ EventListener error for subscription ${subscription.id}:`, error);
this.emit("watcher:error", { subscriptionId: subscription.id, error });
});
// Handle EventProcessor events (optional additional logging)
this.eventProcessor.on("event:processed", (result) => {
console.log(`โ
Event ${result.requestId} processed successfully in ${result.processingTime}ms`);
});
this.eventProcessor.on("event:processing-error", (result) => {
console.error(`โ Event ${result.requestId} processing failed: ${result.error}`);
});
}
/**
* Update processing statistics
*/
updateProcessingStats(success, processingTime) {
this.stats.totalEventsProcessed++;
this.stats.totalProcessingTime += processingTime;
if (success) {
this.stats.totalEventsSuccessful++;
}
else {
this.stats.totalEventsFailed++;
}
}
/**
* Emit stats update event
*/
emitStatsUpdate() {
this.emit("stats:updated", this.getStats());
}
/**
* Cleanup all resources
*/
async cleanup() {
console.log("๐งน Cleaning up EventWatcher...");
try {
// Stop all watching
await this.stopAllWatching();
// Cleanup components
this.eventListener.cleanup();
this.eventProcessor.cleanup();
subscriptionManager.cleanup();
// Reset state
this.isInitialized = false;
this.removeAllListeners();
console.log("โ
EventWatcher cleaned up successfully");
}
catch (error) {
console.error("โ Error during EventWatcher cleanup:", error);
throw error;
}
}
// Type-safe event listener methods
on(event, listener) {
return super.on(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
off(event, listener) {
return super.off(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
}
// Export singleton instance
export const eventWatcher = EventWatcher.getInstance();
//# sourceMappingURL=event-watcher.js.map