UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

1,147 lines (1,146 loc) 40.3 kB
/** * Hocuspocus Provider for Block-Level Collaborative Editing * * Provides real-time synchronization between clients through Hocuspocus server * with enhanced collaboration features, authentication, and conflict resolution. * * Replaces the custom YjsWebSocketProvider with Hocuspocus for better * collaboration infrastructure and enterprise-grade features. */ import { HocuspocusProvider } from '@hocuspocus/provider'; import * as Y from 'yjs'; export class HocuspocusCollaborationProvider { constructor(config) { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "providers", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "documents", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "connectionStates", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "reconnectAttempts", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "listeners", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "authRetryAttempts", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "connectionTimeouts", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "syncTimeouts", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "metrics", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "messageQueue", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "userId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userInfo", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "isOnline", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_networkListeners", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.config = { reconnectInterval: 3000, maxReconnectAttempts: 10, heartbeatInterval: 30000, forceSyncInterval: 5000, quiet: false, preserveConnection: true, // Enhanced defaults retryAuthOnFailure: true, maxAuthRetries: 3, enableAdvancedMessaging: true, connectionTimeout: 10000, syncTimeout: 5000, enableMetrics: true, ...config }; this.userId = config.userId || this.generateUserId(); this.userInfo = config.userInfo || { name: 'Anonymous', color: this.generateUserColor() }; this.isOnline = navigator.onLine; this.setupNetworkListeners(); this.setupPerformanceMonitoring(); } /** * Connect to a block's collaborative session using Hocuspocus with enhanced features */ async connectToBlock(blockId, options = {}) { if (this.providers.has(blockId)) { console.warn(`Already connected to block ${blockId}`); return this.providers.get(blockId); } // Create Y.Doc for this block const ydoc = new Y.Doc(); this.documents.set(blockId, ydoc); // Initialize metrics for this block if (this.config.enableMetrics) { this.initializeBlockMetrics(blockId); } try { // Set connection timeout const connectionTimeout = setTimeout(() => { this.handleConnectionTimeout(blockId); }, this.config.connectionTimeout); this.connectionTimeouts.set(blockId, connectionTimeout); // Create Hocuspocus provider for this specific block const provider = new HocuspocusProvider({ url: this.config.serverUrl, name: `block-${blockId}`, document: ydoc, token: await this.getValidToken(blockId), parameters: { userId: this.userId, userInfo: JSON.stringify(this.userInfo), blockId, timestamp: Date.now(), clientVersion: '1.0.0', ...this.config.parameters, ...options.parameters }, onAuthenticated: (data) => { this.handleAuthenticated(blockId, data); }, onAuthenticationFailed: (data) => { this.handleAuthenticationFailed(blockId, data); }, onStateless: (payload) => { this.handleStatelessMessage(blockId, payload); }, forceSyncInterval: this.config.forceSyncInterval, quiet: this.config.quiet, preserveConnection: this.config.preserveConnection, ...options }); // Store provider and initialize state this.providers.set(blockId, provider); this.connectionStates.set(blockId, 'connecting'); this.reconnectAttempts.set(blockId, 0); this.authRetryAttempts.set(blockId, 0); // Set enhanced awareness state provider.setAwarenessField('user', { id: this.userId, ...this.userInfo, cursor: null, selection: null, lastSeen: Date.now(), isTyping: false, isActive: true, capabilities: ['read', 'write', 'comment'], clientInfo: { userAgent: navigator.userAgent, platform: navigator.platform, language: navigator.language } }); // Set up enhanced event listeners for this block this.setupEnhancedBlockEventListeners(blockId, provider, ydoc); // Clear connection timeout on successful connection clearTimeout(connectionTimeout); this.connectionTimeouts.delete(blockId); return provider; } catch (error) { console.error('Failed to create HocuspocusProvider:', error); this.connectionStates.set(blockId, 'error'); this.emit('connection-error', { blockId, error }); // Clean up on error this.cleanupBlockResources(blockId); throw error; } } /** * Disconnect from a block's collaborative session */ disconnectFromBlock(blockId) { const provider = this.providers.get(blockId); if (!provider) { console.warn(`Not connected to block ${blockId}`); return; } try { provider.destroy(); } catch (error) { console.warn('Error destroying provider:', error); } // Clean up resources this.providers.delete(blockId); this.documents.delete(blockId); this.reconnectAttempts.delete(blockId); this.connectionStates.delete(blockId); this.emit('block-disconnected', { blockId }); } /** * Get the Y.Doc for a specific block */ getDocument(blockId) { return this.documents.get(blockId); } /** * Get the Hocuspocus provider for a specific block */ getProvider(blockId) { return this.providers.get(blockId); } /** * Get connection state for a block */ getConnectionState(blockId) { return this.connectionStates.get(blockId) || 'disconnected'; } /** * Update user awareness information */ updateAwareness(blockId, updates) { const provider = this.providers.get(blockId); if (!provider) { console.warn(`No provider instance for block ${blockId}`); return; } try { const currentState = provider.awareness.getLocalState() || {}; const newState = { ...currentState, ...updates, lastSeen: Date.now() }; provider.awareness.setLocalState(newState); } catch (error) { console.warn('Failed to update awareness:', error); } } /** * Set cursor position for a block */ setCursor(blockId, cursor) { this.updateAwareness(blockId, { cursor }); } /** * Set selection range for a block */ setSelection(blockId, selection) { this.updateAwareness(blockId, { selection }); } /** * Get all connected users for a block */ getConnectedUsers(blockId) { const provider = this.providers.get(blockId); if (!provider) return []; const users = []; try { provider.awareness.getStates().forEach((state, clientId) => { if (clientId !== provider.awareness.clientID && state.user) { users.push({ clientId: clientId.toString(), ...state.user, isActive: Date.now() - (state.lastSeen || 0) < 60000 // Active in last minute }); } }); } catch (error) { console.warn('Failed to get connected users:', error); } return users; } /** * Send a stateless message to other clients with enhanced features */ sendStatelessMessage(blockId, payload, options = {}) { const provider = this.providers.get(blockId); if (!provider) { console.warn(`No provider instance for block ${blockId}`); return; } try { const message = { type: payload.type || 'custom-message', blockId, userId: this.userId, timestamp: Date.now(), messageId: options.messageId || this.generateMessageId(), priority: options.priority || 'normal', reliable: options.reliable || false, targetUsers: options.targetUsers, payload }; provider.sendStateless(JSON.stringify(message)); // Update metrics if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, 'messagesSent', (this.metrics.get(blockId)?.messagesSent || 0) + 1); } this.emit('message-sent', { blockId, message }); } catch (error) { console.warn('Failed to send stateless message:', error); this.emit('message-send-failed', { blockId, error, payload }); } } /** * Send user activity update */ sendUserActivity(blockId, activity) { this.sendStatelessMessage(blockId, { type: 'user-activity', data: { ...activity, userId: this.userId, userInfo: this.userInfo } }, { priority: 'low' }); } /** * Send document event */ sendDocumentEvent(blockId, event) { this.sendStatelessMessage(blockId, { type: 'document-event', data: { ...event, userId: this.userId, timestamp: Date.now() } }, { priority: 'normal', reliable: true }); } /** * Send collaboration metric */ sendCollaborationMetric(blockId, metric, value) { this.sendStatelessMessage(blockId, { type: 'collaboration-metric', metric, value, userId: this.userId }, { priority: 'low' }); } /** * Generate unique message ID */ generateMessageId() { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Collaborative undo functionality */ undoCollaborative(blockId) { const ydoc = this.documents.get(blockId); if (!ydoc) { console.warn(`No document found for block ${blockId}`); return false; } try { // Use Yjs built-in undo manager const undoManager = ydoc._undoManager; if (undoManager && undoManager.canUndo()) { undoManager.undo(); // Send undo event this.sendStatelessMessage(blockId, { type: 'document-event', data: { type: 'undo', userId: this.userId, timestamp: Date.now() } }, { priority: 'normal', reliable: true }); this.emit('collaborative-undo', { blockId, userId: this.userId }); return true; } return false; } catch (error) { console.error('Collaborative undo failed:', error); return false; } } /** * Collaborative redo functionality */ redoCollaborative(blockId) { const ydoc = this.documents.get(blockId); if (!ydoc) { console.warn(`No document found for block ${blockId}`); return false; } try { // Use Yjs built-in undo manager const undoManager = ydoc._undoManager; if (undoManager && undoManager.canRedo()) { undoManager.redo(); // Send redo event this.sendStatelessMessage(blockId, { type: 'document-event', data: { type: 'redo', userId: this.userId, timestamp: Date.now() } }, { priority: 'normal', reliable: true }); this.emit('collaborative-redo', { blockId, userId: this.userId }); return true; } return false; } catch (error) { console.error('Collaborative redo failed:', error); return false; } } /** * Get undo/redo state for a block */ getUndoRedoState(blockId) { const ydoc = this.documents.get(blockId); if (!ydoc) { return { canUndo: false, canRedo: false }; } try { const undoManager = ydoc._undoManager; return { canUndo: undoManager ? undoManager.canUndo() : false, canRedo: undoManager ? undoManager.canRedo() : false }; } catch (error) { console.warn('Failed to get undo/redo state:', error); return { canUndo: false, canRedo: false }; } } /** * Enable offline support for a block */ enableOfflineSupport(blockId) { const ydoc = this.documents.get(blockId); if (!ydoc) { console.warn(`No document found for block ${blockId}`); return; } try { // Store document state in localStorage for offline access const storeKey = `claritykit_offline_${blockId}`; // Save current state const saveState = () => { const state = Y.encodeStateAsUpdate(ydoc); localStorage.setItem(storeKey, JSON.stringify({ state: Array.from(state), timestamp: Date.now(), userId: this.userId })); }; // Save on document updates ydoc.on('update', saveState); // Initial save saveState(); // Store cleanup function ydoc._offlineCleanup = () => { ydoc.off('update', saveState); }; this.emit('offline-support-enabled', { blockId }); } catch (error) { console.error('Failed to enable offline support:', error); } } /** * Load offline changes for a block */ loadOfflineChanges(blockId) { const storeKey = `claritykit_offline_${blockId}`; try { const stored = localStorage.getItem(storeKey); if (!stored) { return false; } const { state, timestamp, userId } = JSON.parse(stored); const ydoc = this.documents.get(blockId); if (!ydoc) { console.warn(`No document found for block ${blockId}`); return false; } // Apply offline changes const update = new Uint8Array(state); Y.applyUpdate(ydoc, update); // Send sync event this.sendStatelessMessage(blockId, { type: 'document-event', data: { type: 'offline-sync', userId, offlineTimestamp: timestamp, syncTimestamp: Date.now() } }, { priority: 'high', reliable: true }); this.emit('offline-changes-loaded', { blockId, timestamp, userId }); return true; } catch (error) { console.error('Failed to load offline changes:', error); return false; } } /** * Clear offline data for a block */ clearOfflineData(blockId) { const storeKey = `claritykit_offline_${blockId}`; localStorage.removeItem(storeKey); this.emit('offline-data-cleared', { blockId }); } /** * Force synchronization for a block */ forceSync(blockId) { const provider = this.providers.get(blockId); if (!provider) { console.warn(`No provider instance for block ${blockId}`); return; } try { provider.forceSync(); } catch (error) { console.warn('Failed to force sync:', error); } } /** * Setup enhanced event listeners for a block's provider and document */ setupEnhancedBlockEventListeners(blockId, provider, ydoc) { // Connection status events provider.on('status', ({ status }) => { let connectionState; switch (status) { case 'connected': connectionState = 'connected'; this.reconnectAttempts.set(blockId, 0); this.emit('block-connected', { blockId }); break; case 'connecting': connectionState = 'connecting'; break; case 'disconnected': connectionState = 'disconnected'; this.emit('block-disconnected', { blockId }); this.handleReconnection(blockId); break; default: connectionState = 'error'; } this.connectionStates.set(blockId, connectionState); this.emit('connection-status-changed', { blockId, status: connectionState }); }); // Sync events provider.on('synced', () => { this.connectionStates.set(blockId, 'synced'); this.emit('block-synced', { blockId }); }); // Document update events ydoc.on('update', (update, origin) => { this.emit('document-updated', { blockId, update, origin }); }); // Awareness events provider.awareness.on('update', ({ added, updated, removed }) => { this.emit('awareness-updated', { blockId, added, updated, removed, users: this.getConnectedUsers(blockId) }); }); // Authentication events provider.on('authenticated', (data) => { this.emit('block-authenticated', { blockId, data }); }); provider.on('authenticationFailed', (data) => { this.emit('block-authentication-failed', { blockId, data }); }); // Stateless message events provider.on('stateless', (payload) => { try { const message = JSON.parse(payload); if (message.blockId === blockId) { this.emit('stateless-message-received', { blockId, message }); } } catch (error) { console.warn('Failed to parse stateless message:', error); } }); // Error events provider.on('close', ({ event }) => { console.warn(`Connection closed for block ${blockId}:`, event); this.emit('connection-closed', { blockId, event }); // Update metrics if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, 'errors', (this.metrics.get(blockId)?.errors || 0) + 1); } }); // Enhanced sync monitoring if (this.config.syncTimeout) { const syncTimeout = setTimeout(() => { console.warn(`Sync timeout for block ${blockId}`); this.emit('sync-timeout', { blockId }); }, this.config.syncTimeout); this.syncTimeouts.set(blockId, syncTimeout); // Clear timeout when synced provider.on('synced', () => { const timeout = this.syncTimeouts.get(blockId); if (timeout) { clearTimeout(timeout); this.syncTimeouts.delete(blockId); } }); } // Enhanced metrics collection if (this.config.enableMetrics) { // Track sync events provider.on('synced', () => { this.updateBlockMetrics(blockId, 'syncCount', (this.metrics.get(blockId)?.syncCount || 0) + 1); }); // Track message events provider.on('stateless', () => { this.updateBlockMetrics(blockId, 'messagesReceived', (this.metrics.get(blockId)?.messagesReceived || 0) + 1); }); } // Enhanced error handling provider.on('error', ({ error }) => { console.error(`Provider error for block ${blockId}:`, error); this.emit('provider-error', { blockId, error }); if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, 'errors', (this.metrics.get(blockId)?.errors || 0) + 1); } }); } /** * Handle automatic reconnection for a block */ async handleReconnection(blockId) { if (!this.isOnline) return; const attempts = this.reconnectAttempts.get(blockId) || 0; if (attempts >= this.config.maxReconnectAttempts) { this.emit('reconnection-failed', { blockId, attempts }); return; } this.reconnectAttempts.set(blockId, attempts + 1); this.connectionStates.set(blockId, 'reconnecting'); const delay = this.config.reconnectInterval * Math.pow(1.5, attempts); // Exponential backoff setTimeout(() => { const provider = this.providers.get(blockId); if (provider && provider.status !== 'connected') { this.emit('reconnecting', { blockId, attempt: attempts + 1 }); try { provider.connect(); } catch (error) { console.warn('Reconnection attempt failed:', error); } } }, delay); } /** * Setup network connectivity listeners */ setupNetworkListeners() { const handleOnline = () => { this.isOnline = true; this.emit('network-online'); // Attempt to reconnect all providers this.providers.forEach((provider, blockId) => { if (provider.status !== 'connected') { this.handleReconnection(blockId); } }); }; const handleOffline = () => { this.isOnline = false; this.emit('network-offline'); }; window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // Store references for cleanup this._networkListeners = { handleOnline, handleOffline }; } /** * Get enhanced statistics about all connections */ getStats() { const stats = { totalBlocks: this.providers.size, connectedBlocks: 0, syncedBlocks: 0, totalUsers: 0, isOnline: this.isOnline, syncErrors: 0 }; let totalLatency = 0; let latencyCount = 0; let totalMessages = 0; let totalSyncs = 0; this.connectionStates.forEach((state) => { if (state === 'connected' || state === 'synced') { stats.connectedBlocks++; } if (state === 'synced') { stats.syncedBlocks++; } }); this.providers.forEach((provider, blockId) => { try { stats.totalUsers += Math.max(0, provider.awareness.getStates().size - 1); // Exclude self // Collect metrics if available const blockMetrics = this.metrics.get(blockId); if (blockMetrics) { totalMessages += (blockMetrics.messagesSent || 0) + (blockMetrics.messagesReceived || 0); totalSyncs += blockMetrics.syncCount || 0; stats.syncErrors += blockMetrics.errors || 0; // Calculate latency if available if (blockMetrics.connectionTime) { const latency = Date.now() - blockMetrics.connectionTime; totalLatency += latency; latencyCount++; } } } catch (error) { console.warn('Failed to get awareness stats:', error); stats.syncErrors++; } }); // Calculate average latency if (latencyCount > 0) { stats.averageLatency = Math.round(totalLatency / latencyCount); } // Add enhanced stats stats.totalMessages = totalMessages; stats.totalSyncs = totalSyncs; stats.authRetries = Array.from(this.authRetryAttempts.values()).reduce((sum, retries) => sum + retries, 0); stats.queuedMessages = Array.from(this.messageQueue.values()).reduce((sum, queue) => sum + queue.length, 0); return stats; } /** * Get detailed metrics for a specific block */ getBlockStats(blockId) { const provider = this.providers.get(blockId); const metrics = this.metrics.get(blockId); const connectionState = this.connectionStates.get(blockId); if (!provider) { return null; } const stats = { blockId, connectionState, isConnected: provider.status === 'connected', connectedUsers: this.getConnectedUsers(blockId).length, reconnectAttempts: this.reconnectAttempts.get(blockId) || 0, authRetryAttempts: this.authRetryAttempts.get(blockId) || 0, queuedMessages: this.messageQueue.get(blockId)?.length || 0, ...metrics }; return stats; } /** * Disconnect from all blocks and cleanup all resources */ destroy() { // Disconnect from all blocks Array.from(this.providers.keys()).forEach(blockId => { this.disconnectFromBlock(blockId); }); // Clear all timeouts this.connectionTimeouts.forEach(timeout => clearTimeout(timeout)); this.syncTimeouts.forEach(timeout => clearTimeout(timeout)); // Remove network listeners if (this._networkListeners) { window.removeEventListener('online', this._networkListeners.handleOnline); window.removeEventListener('offline', this._networkListeners.handleOffline); } // Clear all maps and resources this.providers.clear(); this.documents.clear(); this.reconnectAttempts.clear(); this.connectionStates.clear(); this.authRetryAttempts.clear(); this.connectionTimeouts.clear(); this.syncTimeouts.clear(); this.metrics.clear(); this.messageQueue.clear(); // Clear event listeners this.listeners = {}; this.emit('provider-destroyed'); } /** * Simple event emitter implementation */ emit(event, data) { if (this.listeners[event]) { this.listeners[event].forEach(callback => { try { callback(data); } catch (error) { console.error('Event listener error:', error); } }); } } on(event, callback) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(callback); } off(event, callback) { if (!this.listeners[event]) return; const index = this.listeners[event].indexOf(callback); if (index > -1) { this.listeners[event].splice(index, 1); } } /** * Enhanced authentication handling */ async getValidToken(blockId) { let token = this.config.token; // If no token provided, return undefined if (!token) { return undefined; } // Check if token needs refresh if (this.isTokenExpired(token) && this.config.tokenRefreshCallback) { try { token = await this.config.tokenRefreshCallback(); this.config.token = token; // Update stored token this.emit('token-refreshed', { blockId, token }); } catch (error) { console.error('Token refresh failed:', error); this.emit('token-refresh-failed', { blockId, error }); throw error; } } return token; } /** * Check if JWT token is expired */ isTokenExpired(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Math.floor(Date.now() / 1000); return payload.exp && payload.exp < currentTime; } catch (error) { console.warn('Failed to parse token:', error); return false; // Assume valid if can't parse } } /** * Handle successful authentication */ handleAuthenticated(blockId, data) { console.log(`Authenticated for block ${blockId}:`, data); this.authRetryAttempts.set(blockId, 0); // Reset retry count this.config.onAuthenticated?.(data); this.emit('block-authenticated', { blockId, data }); // Update metrics if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, 'authSuccess', Date.now()); } } /** * Handle authentication failure with retry logic */ async handleAuthenticationFailed(blockId, data) { console.error(`Authentication failed for block ${blockId}:`, data); const retryCount = this.authRetryAttempts.get(blockId) || 0; if (this.config.retryAuthOnFailure && retryCount < (this.config.maxAuthRetries || 3)) { this.authRetryAttempts.set(blockId, retryCount + 1); // Try to refresh token and reconnect try { if (this.config.tokenRefreshCallback) { const newToken = await this.config.tokenRefreshCallback(); this.config.token = newToken; // Reconnect with new token setTimeout(() => { this.reconnectBlock(blockId); }, 1000 * (retryCount + 1)); // Exponential backoff return; } } catch (error) { console.error('Token refresh during auth retry failed:', error); } } // Max retries reached or no retry enabled this.config.onAuthenticationFailed?.(data); this.emit('block-authentication-failed', { blockId, data }); // Update metrics if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, 'authFailure', Date.now()); } } /** * Handle stateless messages with advanced processing */ handleStatelessMessage(blockId, payload) { try { let message = payload; // Parse JSON if it's a string if (typeof payload === 'string') { message = JSON.parse(payload); } // Enhanced message processing if (this.config.enableAdvancedMessaging) { this.processAdvancedMessage(blockId, message); } this.config.onStateless?.(message); this.emit('stateless-message', { blockId, payload: message }); } catch (error) { console.warn('Failed to process stateless message:', error); } } /** * Process advanced stateless messages */ processAdvancedMessage(blockId, message) { switch (message.type) { case 'user-activity': this.handleUserActivity(blockId, message); break; case 'document-event': this.handleDocumentEvent(blockId, message); break; case 'collaboration-metric': this.handleCollaborationMetric(blockId, message); break; case 'presence-update': this.handlePresenceUpdate(blockId, message); break; default: // Queue unknown messages for later processing this.queueMessage(blockId, message); } } /** * Handle user activity messages */ handleUserActivity(blockId, message) { this.emit('user-activity', { blockId, activity: message.data }); } /** * Handle document event messages */ handleDocumentEvent(blockId, message) { this.emit('document-event', { blockId, event: message.data }); } /** * Handle collaboration metrics */ handleCollaborationMetric(blockId, message) { if (this.config.enableMetrics) { this.updateBlockMetrics(blockId, message.metric, message.value); } } /** * Handle presence updates */ handlePresenceUpdate(blockId, message) { this.emit('presence-update', { blockId, presence: message.data }); } /** * Queue messages for later processing */ queueMessage(blockId, message) { if (!this.messageQueue.has(blockId)) { this.messageQueue.set(blockId, []); } this.messageQueue.get(blockId).push(message); // Limit queue size const queue = this.messageQueue.get(blockId); if (queue.length > 100) { queue.shift(); // Remove oldest message } } /** * Handle connection timeout */ handleConnectionTimeout(blockId) { console.warn(`Connection timeout for block ${blockId}`); this.connectionStates.set(blockId, 'error'); this.emit('connection-timeout', { blockId }); // Clean up timeout this.connectionTimeouts.delete(blockId); } /** * Initialize performance monitoring */ setupPerformanceMonitoring() { if (!this.config.enableMetrics) return; // Monitor overall performance setInterval(() => { const stats = this.getStats(); this.emit('performance-metrics', stats); }, 30000); // Every 30 seconds } /** * Initialize metrics for a block */ initializeBlockMetrics(blockId) { this.metrics.set(blockId, { connectionTime: Date.now(), authAttempts: 0, authSuccess: null, authFailure: null, syncCount: 0, messagesSent: 0, messagesReceived: 0, errors: 0, lastActivity: Date.now() }); } /** * Update block metrics */ updateBlockMetrics(blockId, metric, value) { const blockMetrics = this.metrics.get(blockId); if (blockMetrics) { blockMetrics[metric] = value; blockMetrics.lastActivity = Date.now(); } } /** * Reconnect to a specific block */ async reconnectBlock(blockId) { const provider = this.providers.get(blockId); if (provider) { try { // Update token if available const newToken = await this.getValidToken(blockId); if (newToken) { // Update provider token (if supported by Hocuspocus) provider.token = newToken; } provider.connect(); this.emit('block-reconnecting', { blockId }); } catch (error) { console.error('Failed to reconnect block:', error); this.emit('block-reconnect-failed', { blockId, error }); } } } /** * Clean up resources for a block */ cleanupBlockResources(blockId) { // Clear timeouts const connectionTimeout = this.connectionTimeouts.get(blockId); if (connectionTimeout) { clearTimeout(connectionTimeout); this.connectionTimeouts.delete(blockId); } const syncTimeout = this.syncTimeouts.get(blockId); if (syncTimeout) { clearTimeout(syncTimeout); this.syncTimeouts.delete(blockId); } // Clear metrics this.metrics.delete(blockId); // Clear message queue this.messageQueue.delete(blockId); // Reset retry attempts this.authRetryAttempts.delete(blockId); } /** * Generate a unique user ID */ generateUserId() { return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Generate a random user color */ generateUserColor() { const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3', '#54A0FF', '#5F27CD', '#00D2D3', '#FF9F43', '#EE5A6F', '#0ABDE3' ]; return colors[Math.floor(Math.random() * colors.length)]; } } export default HocuspocusCollaborationProvider;