UNPKG

@semantest/chrome-extension

Version:

Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework

581 lines (580 loc) 19.5 kB
/** * @fileoverview Plugin Communication System for Web-Buddy plugin architecture * @description Implements plugin messaging, event bus, and inter-plugin communication */ import { PluginError } from './plugin-interface'; /** * Message types for plugin communication */ export var PluginMessageType; (function (PluginMessageType) { PluginMessageType["DIRECT_MESSAGE"] = "direct-message"; PluginMessageType["BROADCAST"] = "broadcast"; PluginMessageType["REQUEST"] = "request"; PluginMessageType["RESPONSE"] = "response"; PluginMessageType["PUBLISH"] = "publish"; PluginMessageType["SUBSCRIBE"] = "subscribe"; PluginMessageType["UNSUBSCRIBE"] = "unsubscribe"; })(PluginMessageType || (PluginMessageType = {})); /** * Event bus implementation with filtering and history */ export class DefaultPluginEventBus { constructor() { this.eventHandlers = new Map(); this.eventHistory = []; this.filters = []; this.transformers = []; this.maxHistorySize = 1000; } /** * Emit an event to all registered handlers */ async emit(event) { try { // Apply filters if (!this.passesFilters(event)) { return; } // Apply transformers const transformedEvent = this.applyTransformers(event); // Add to history this.addToHistory(transformedEvent); // Get handlers for this event type const handlers = this.eventHandlers.get(transformedEvent.type) || new Set(); // Execute all handlers const promises = Array.from(handlers).map(async (handler) => { try { await handler(transformedEvent); } catch (error) { console.error(`Error in event handler for ${transformedEvent.type}:`, error); // Don't throw here to avoid stopping other handlers } }); await Promise.allSettled(promises); } catch (error) { console.error('Error emitting event:', error); throw new PluginError(`Failed to emit event ${event.type}: ${error.message}`, event.source, 'EVENT_EMISSION_ERROR', error); } } /** * Register an event handler */ on(eventType, handler) { if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, new Set()); } this.eventHandlers.get(eventType).add(handler); } /** * Unregister an event handler */ off(eventType, handler) { const handlers = this.eventHandlers.get(eventType); if (handlers) { handlers.delete(handler); if (handlers.size === 0) { this.eventHandlers.delete(eventType); } } } /** * Register a one-time event handler */ once(eventType, handler) { const onceHandler = async (event) => { this.off(eventType, onceHandler); await handler(event); }; this.on(eventType, onceHandler); } /** * Create a filtered event bus */ filter(predicate) { const filteredBus = new DefaultPluginEventBus(); filteredBus.filters = [...this.filters, predicate]; filteredBus.transformers = [...this.transformers]; filteredBus.eventHandlers = new Map(this.eventHandlers); return filteredBus; } /** * Create a transformed event bus */ pipe(transformer) { const pipedBus = new DefaultPluginEventBus(); pipedBus.filters = [...this.filters]; pipedBus.transformers = [...this.transformers, transformer]; pipedBus.eventHandlers = new Map(this.eventHandlers); return pipedBus; } /** * Get event history */ getHistory(pluginId) { if (pluginId) { return this.eventHistory.filter(event => event.source === pluginId || event.target === pluginId); } return [...this.eventHistory]; } /** * Replay events from a timestamp */ async replay(fromTimestamp) { const targetTime = fromTimestamp ? new Date(fromTimestamp) : new Date(0); const eventsToReplay = this.eventHistory.filter(event => new Date(event.timestamp) >= targetTime); for (const event of eventsToReplay) { await this.emit({ ...event, type: `replay:${event.type}`, timestamp: new Date().toISOString() }); } } /** * Clear event history */ clearHistory() { this.eventHistory = []; } /** * Get event statistics */ getStatistics() { const stats = { totalEvents: this.eventHistory.length, handlerCount: 0, eventTypes: new Set(), filters: this.filters.length, transformers: this.transformers.length }; for (const handlers of this.eventHandlers.values()) { stats.handlerCount += handlers.size; } for (const event of this.eventHistory) { stats.eventTypes.add(event.type); } return { ...stats, eventTypes: Array.from(stats.eventTypes) }; } // Private helper methods passesFilters(event) { return this.filters.every(filter => filter(event)); } applyTransformers(event) { return this.transformers.reduce((transformedEvent, transformer) => transformer(transformedEvent), event); } addToHistory(event) { this.eventHistory.push(event); // Trim history if it exceeds max size if (this.eventHistory.length > this.maxHistorySize) { this.eventHistory = this.eventHistory.slice(-this.maxHistorySize); } } } /** * Plugin messaging implementation */ export class DefaultPluginMessaging { constructor(eventBus) { this.eventBus = eventBus; this.subscriptions = new Map(); this.pendingRequests = new Map(); this.messageHandlers = new Map(); this.requestTimeout = 30000; // 30 seconds this.setupSystemHandlers(); } /** * Send a direct message to another plugin */ async sendMessage(fromPlugin, toPlugin, message) { const pluginMessage = { id: this.generateMessageId(), type: PluginMessageType.DIRECT_MESSAGE, from: fromPlugin, to: toPlugin, data: message, timestamp: new Date().toISOString() }; return this.deliverMessage(pluginMessage); } /** * Publish a message to a topic */ async publish(topic, data) { const subscriptions = this.subscriptions.get(topic) || new Set(); const publishEvent = { type: 'plugin:topic:published', source: 'messaging-system', data: { topic, data }, timestamp: new Date().toISOString() }; await this.eventBus.emit(publishEvent); // Deliver to all subscribers const promises = Array.from(subscriptions).map(async (subscription) => { try { const event = { type: `topic:${topic}`, source: 'messaging-system', target: subscription.pluginId, data, timestamp: new Date().toISOString() }; await subscription.handler(event); } catch (error) { console.error(`Error delivering topic message to ${subscription.pluginId}:`, error); } }); await Promise.allSettled(promises); } /** * Subscribe to a topic */ subscribe(topic, handler) { // Extract plugin ID from the handler context (this is a simplified approach) const pluginId = this.extractPluginIdFromHandler(handler) || 'unknown'; const subscription = { pluginId, topic, handler, created: new Date() }; if (!this.subscriptions.has(topic)) { this.subscriptions.set(topic, new Set()); } this.subscriptions.get(topic).add(subscription); // Emit subscription event this.eventBus.emit({ type: 'plugin:topic:subscribed', source: 'messaging-system', target: pluginId, data: { topic, pluginId }, timestamp: new Date().toISOString() }); } /** * Unsubscribe from a topic */ unsubscribe(topic, handler) { const subscriptions = this.subscriptions.get(topic); if (!subscriptions) return; const toRemove = Array.from(subscriptions).find(sub => sub.handler === handler); if (toRemove) { subscriptions.delete(toRemove); if (subscriptions.size === 0) { this.subscriptions.delete(topic); } // Emit unsubscription event this.eventBus.emit({ type: 'plugin:topic:unsubscribed', source: 'messaging-system', target: toRemove.pluginId, data: { topic, pluginId: toRemove.pluginId }, timestamp: new Date().toISOString() }); } } /** * Send a request and wait for response */ async request(pluginId, request) { const requestId = this.generateMessageId(); const requestMessage = { id: this.generateMessageId(), type: PluginMessageType.REQUEST, from: 'messaging-system', // This should be set to the actual requesting plugin to: pluginId, data: request, timestamp: new Date().toISOString(), requestId }; return new Promise((resolve, reject) => { // Set up timeout const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new Error(`Request timeout: ${pluginId} did not respond within ${this.requestTimeout}ms`)); }, this.requestTimeout); // Store pending request const pendingRequest = { requestId, from: requestMessage.from, to: pluginId, timestamp: new Date(), resolve, reject, timeout }; this.pendingRequests.set(requestId, pendingRequest); // Send the request this.deliverMessage(requestMessage).catch(error => { clearTimeout(timeout); this.pendingRequests.delete(requestId); reject(error); }); }); } /** * Send a response to a request */ async respond(requestId, response) { const pendingRequest = this.pendingRequests.get(requestId); if (!pendingRequest) { throw new Error(`No pending request found for ID: ${requestId}`); } const responseMessage = { id: this.generateMessageId(), type: PluginMessageType.RESPONSE, from: pendingRequest.to, to: pendingRequest.from, data: response, timestamp: new Date().toISOString(), responseId: requestId }; // Clear timeout and resolve pending request if (pendingRequest.timeout) { clearTimeout(pendingRequest.timeout); } this.pendingRequests.delete(requestId); pendingRequest.resolve(response); // Emit response event await this.eventBus.emit({ type: 'plugin:request:responded', source: 'messaging-system', data: { requestId, response }, timestamp: new Date().toISOString() }); } /** * Broadcast a message to all plugins */ async broadcast(message) { const broadcastEvent = { type: 'plugin:broadcast', source: 'messaging-system', data: message, timestamp: new Date().toISOString() }; await this.eventBus.emit(broadcastEvent); } /** * Register a message handler for a plugin */ registerMessageHandler(pluginId, handler) { this.messageHandlers.set(pluginId, handler); } /** * Unregister a message handler */ unregisterMessageHandler(pluginId) { this.messageHandlers.delete(pluginId); } /** * Get messaging statistics */ getStatistics() { const subscriptionStats = new Map(); for (const [topic, subs] of this.subscriptions.entries()) { subscriptionStats.set(topic, subs.size); } return { totalSubscriptions: Array.from(this.subscriptions.values()) .reduce((total, subs) => total + subs.size, 0), topicCount: this.subscriptions.size, pendingRequests: this.pendingRequests.size, registeredHandlers: this.messageHandlers.size, subscriptionsByTopic: Object.fromEntries(subscriptionStats) }; } /** * Clean up expired requests and subscriptions */ cleanup() { const now = Date.now(); const expiredRequests = []; // Find expired requests for (const [requestId, request] of this.pendingRequests.entries()) { if (now - request.timestamp.getTime() > this.requestTimeout) { expiredRequests.push(requestId); } } // Clean up expired requests for (const requestId of expiredRequests) { const request = this.pendingRequests.get(requestId); if (request) { if (request.timeout) { clearTimeout(request.timeout); } request.reject(new Error('Request expired during cleanup')); this.pendingRequests.delete(requestId); } } } // Private helper methods async deliverMessage(message) { if (message.to) { // Direct message delivery const handler = this.messageHandlers.get(message.to); if (handler) { try { return await handler(message); } catch (error) { throw new PluginError(`Message delivery failed to ${message.to}: ${error.message}`, message.from, 'MESSAGE_DELIVERY_ERROR', error); } } else { throw new PluginError(`No message handler registered for plugin: ${message.to}`, message.from, 'NO_MESSAGE_HANDLER'); } } else { // Broadcast message const promises = Array.from(this.messageHandlers.entries()).map(async ([pluginId, handler]) => { try { await handler({ ...message, to: pluginId }); } catch (error) { console.error(`Error delivering broadcast message to ${pluginId}:`, error); } }); await Promise.allSettled(promises); } } generateMessageId() { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } extractPluginIdFromHandler(handler) { // This is a simplified approach. In a real implementation, you might // bind the plugin ID to the handler or use a more sophisticated method return handler.pluginId || null; } setupSystemHandlers() { // Set up periodic cleanup setInterval(() => { this.cleanup(); }, 60000); // Clean up every minute } } /** * Plugin communication factory */ export class PluginCommunicationFactory { constructor() { this.eventBus = new DefaultPluginEventBus(); this.messaging = new DefaultPluginMessaging(this.eventBus); } /** * Get the event bus instance */ getEventBus() { return this.eventBus; } /** * Get the messaging instance */ getMessaging() { return this.messaging; } /** * Create a plugin-specific event bus */ createPluginEventBus(pluginId) { return this.eventBus.filter(event => event.source === pluginId || event.target === pluginId || !event.target // Global events ); } /** * Create a topic-specific event bus */ createTopicEventBus(topic) { return this.eventBus.filter(event => event.type.startsWith(`topic:${topic}`) || event.type === 'plugin:topic:published' || event.type === 'plugin:topic:subscribed' || event.type === 'plugin:topic:unsubscribed'); } /** * Create event bus with custom filters */ createFilteredEventBus(filters) { return filters.reduce((bus, filter) => bus.filter(filter), this.eventBus); } /** * Get communication statistics */ getStatistics() { return { eventBus: this.eventBus.getStatistics(), messaging: this.messaging.getStatistics() }; } } /** * Helper functions for common communication patterns */ export class PluginCommunicationHelpers { /** * Create a plugin-to-plugin communication channel */ static createChannel(messaging, fromPlugin, toPlugin) { return { send: (data) => messaging.sendMessage(fromPlugin, toPlugin, data), request: (data) => messaging.request(toPlugin, data) }; } /** * Create a topic publisher */ static createPublisher(messaging, topic) { return (data) => messaging.publish(topic, data); } /** * Create a topic subscriber */ static createSubscriber(messaging, topic, handler) { messaging.subscribe(topic, handler); return () => messaging.unsubscribe(topic, handler); } /** * Create event filters */ static createFilters() { return { bySource: (pluginId) => (event) => event.source === pluginId, byTarget: (pluginId) => (event) => event.target === pluginId, byType: (eventType) => (event) => event.type === eventType, byPriority: (minPriority) => { const priorities = { low: 0, medium: 1, high: 2, critical: 3 }; const minLevel = priorities[minPriority]; return (event) => { const eventLevel = priorities[event.priority || 'low']; return eventLevel >= minLevel; }; } }; } /** * Create event transformers */ static createTransformers() { return { addPluginPrefix: (pluginId) => (event) => ({ ...event, type: `plugin:${pluginId}:${event.type}` }), addTimestamp: () => (event) => ({ ...event, timestamp: new Date().toISOString() }), addCorrelationId: () => (event) => ({ ...event, correlationId: event.correlationId || `corr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` }) }; } }