UNPKG

optivise

Version:

Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support

585 lines 21 kB
/** * Real-time Collaboration and Workspace Synchronization Service * Enables shared contexts, collaborative workflows, and team synchronization */ import { EventEmitter } from 'events'; export class CollaborationService extends EventEmitter { workspaces = new Map(); activeSessions = new Map(); syncEvents = []; pendingConflicts = new Map(); logger; syncInterval; cleanupInterval; constructor(logger) { super(); this.logger = logger; this.startSyncTimer(); this.startCleanupTimer(); this.logger.info('Collaboration Service initialized'); } /** * Create a new workspace for collaboration */ createWorkspace(config) { const workspace = { id: `workspace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name: config.name, description: config.description, owner: config.owner, collaborators: [config.owner], createdAt: Date.now(), updatedAt: Date.now(), isPublic: config.isPublic || false, settings: { allowEditing: true, requireApproval: false, syncFrequency: 5000, // 5 seconds maxCollaborators: 10, ...config.settings }, data: { sharedRules: [], projectSettings: {}, detectedProducts: [], learningPatterns: [], collaborativeNotes: [] } }; this.workspaces.set(workspace.id, workspace); this.logger.info('Workspace created', { workspaceId: workspace.id, name: workspace.name, owner: workspace.owner }); this.emit('workspaceCreated', workspace); return workspace; } /** * Join a workspace as a collaborator */ joinWorkspace(workspaceId, userId, username, role = 'viewer') { const workspace = this.workspaces.get(workspaceId); if (!workspace) { this.logger.warn('Workspace not found', { workspaceId, userId }); return false; } // Check if user is already a collaborator if (workspace.collaborators.includes(userId)) { this.logger.debug('User already in workspace', { workspaceId, userId }); return true; } // Check workspace limits if (workspace.collaborators.length >= workspace.settings.maxCollaborators) { this.logger.warn('Workspace at capacity', { workspaceId, userId, capacity: workspace.settings.maxCollaborators }); return false; } // Add user to workspace workspace.collaborators.push(userId); workspace.updatedAt = Date.now(); // Create or update session let session = this.activeSessions.get(workspaceId); if (!session) { session = { id: `session_${workspaceId}_${Date.now()}`, workspaceId, participants: [], activeEditors: new Set(), lockState: new Map(), messageHistory: [] }; this.activeSessions.set(workspaceId, session); } // Add participant to session const participant = { userId, username, role: (userId === workspace.owner ? 'owner' : role), joinedAt: Date.now(), lastActivity: Date.now(), isActive: true }; session.participants.push(participant); // Record sync event this.recordSyncEvent({ workspaceId, type: 'user_join', userId, data: { username, role: participant.role } }); this.logger.info('User joined workspace', { workspaceId, userId, username, role: participant.role }); this.emit('userJoined', { workspace, userId, username, role: participant.role }); return true; } /** * Leave a workspace */ leaveWorkspace(workspaceId, userId) { const workspace = this.workspaces.get(workspaceId); const session = this.activeSessions.get(workspaceId); if (workspace) { // Remove from collaborators (unless owner) if (userId !== workspace.owner) { workspace.collaborators = workspace.collaborators.filter(id => id !== userId); workspace.updatedAt = Date.now(); } } if (session) { // Remove from active session session.participants = session.participants.filter(p => p.userId !== userId); session.activeEditors.delete(userId); // Release any locks held by this user for (const [lockId, lock] of session.lockState) { if (lock.userId === userId) { session.lockState.delete(lockId); } } } // Record sync event this.recordSyncEvent({ workspaceId, type: 'user_leave', userId, data: {} }); this.logger.info('User left workspace', { workspaceId, userId }); this.emit('userLeft', { workspaceId, userId }); return true; } /** * Share a rule with the workspace */ shareRule(workspaceId, userId, rule) { const workspace = this.workspaces.get(workspaceId); if (!workspace || !this.canUserEdit(workspaceId, userId)) { return false; } const sharedRule = { id: `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name: rule.name, content: rule.content, author: userId, version: 1, lastModified: Date.now() }; workspace.data.sharedRules.push(sharedRule); workspace.updatedAt = Date.now(); // Record sync event this.recordSyncEvent({ workspaceId, type: 'rule_update', userId, data: { action: 'add', rule: sharedRule } }); this.logger.info('Rule shared in workspace', { workspaceId, userId, ruleId: sharedRule.id }); this.emit('ruleShared', { workspace, rule: sharedRule, userId }); return true; } /** * Update a shared rule */ updateSharedRule(workspaceId, userId, ruleId, updates) { const workspace = this.workspaces.get(workspaceId); if (!workspace || !this.canUserEdit(workspaceId, userId)) { return false; } const rule = workspace.data.sharedRules.find(r => r.id === ruleId); if (!rule) { return false; } // Check for edit conflicts if (this.hasEditConflict(workspaceId, ruleId, userId)) { this.handleEditConflict(workspaceId, ruleId, userId, updates); return false; } // Apply updates if (updates.name) rule.name = updates.name; if (updates.content) rule.content = updates.content; rule.version++; rule.lastModified = Date.now(); workspace.updatedAt = Date.now(); // Record sync event this.recordSyncEvent({ workspaceId, type: 'rule_update', userId, data: { action: 'update', ruleId, updates, version: rule.version } }); this.logger.info('Shared rule updated', { workspaceId, userId, ruleId, version: rule.version }); this.emit('ruleUpdated', { workspace, rule, userId }); return true; } /** * Add a collaborative note */ addNote(workspaceId, userId, note) { const workspace = this.workspaces.get(workspaceId); if (!workspace?.collaborators.includes(userId)) { return false; } const collaborativeNote = { id: `note_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, content: note.content, author: userId, timestamp: Date.now(), tags: note.tags || [] }; workspace.data.collaborativeNotes.push(collaborativeNote); workspace.updatedAt = Date.now(); // Record sync event this.recordSyncEvent({ workspaceId, type: 'note_add', userId, data: { note: collaborativeNote } }); this.logger.debug('Note added to workspace', { workspaceId, userId, noteId: collaborativeNote.id }); this.emit('noteAdded', { workspace, note: collaborativeNote, userId }); return true; } /** * Share a learning pattern with the workspace */ sharePattern(workspaceId, userId, pattern) { const workspace = this.workspaces.get(workspaceId); if (!workspace?.collaborators.includes(userId)) { return false; } // Check if pattern already exists const existingPattern = workspace.data.learningPatterns.find(p => p.pattern === pattern.pattern); if (existingPattern) { existingPattern.votes++; existingPattern.confidence = Math.max(existingPattern.confidence, pattern.confidence); } else { workspace.data.learningPatterns.push({ pattern: pattern.pattern, confidence: pattern.confidence, sharedBy: userId, votes: 1 }); } workspace.updatedAt = Date.now(); // Record sync event this.recordSyncEvent({ workspaceId, type: 'pattern_share', userId, data: { pattern: pattern.pattern, confidence: pattern.confidence } }); this.logger.debug('Pattern shared in workspace', { workspaceId, userId, pattern: pattern.pattern }); this.emit('patternShared', { workspace, pattern, userId }); return true; } /** * Get workspace information */ getWorkspace(workspaceId) { return this.workspaces.get(workspaceId) || null; } /** * Get active session for workspace */ getSession(workspaceId) { return this.activeSessions.get(workspaceId) || null; } /** * Get workspaces for a user */ getUserWorkspaces(userId) { const workspaces = []; for (const workspace of this.workspaces.values()) { if (workspace.collaborators.includes(userId) || workspace.isPublic) { workspaces.push(workspace); } } return workspaces.sort((a, b) => b.updatedAt - a.updatedAt); } /** * Get recent sync events for a workspace */ getRecentEvents(workspaceId, limit = 50) { return this.syncEvents .filter(event => event.workspaceId === workspaceId) .sort((a, b) => b.timestamp - a.timestamp) .slice(0, limit); } /** * Acquire edit lock for a resource */ acquireLock(workspaceId, userId, resource) { const session = this.activeSessions.get(workspaceId); if (!session || !this.canUserEdit(workspaceId, userId)) { return false; } const lockId = `${workspaceId}_${resource}`; const existingLock = session.lockState.get(lockId); // Check if resource is already locked by another user if (existingLock && existingLock.userId !== userId) { const lockAge = Date.now() - existingLock.timestamp; // Auto-release locks after 5 minutes of inactivity if (lockAge < 5 * 60 * 1000) { return false; } } // Acquire lock session.lockState.set(lockId, { userId, timestamp: Date.now(), resource }); session.activeEditors.add(userId); this.logger.debug('Edit lock acquired', { workspaceId, userId, resource }); this.emit('lockAcquired', { workspaceId, userId, resource }); return true; } /** * Release edit lock */ releaseLock(workspaceId, userId, resource) { const session = this.activeSessions.get(workspaceId); if (!session) { return false; } const lockId = `${workspaceId}_${resource}`; const lock = session.lockState.get(lockId); if (lock && lock.userId === userId) { session.lockState.delete(lockId); // Check if user has any other locks const hasOtherLocks = Array.from(session.lockState.values()).some(l => l.userId === userId); if (!hasOtherLocks) { session.activeEditors.delete(userId); } this.logger.debug('Edit lock released', { workspaceId, userId, resource }); this.emit('lockReleased', { workspaceId, userId, resource }); return true; } return false; } /** * Send message to workspace chat */ sendMessage(workspaceId, userId, message, type = 'chat') { const session = this.activeSessions.get(workspaceId); if (!session) { return false; } const chatMessage = { id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, userId, message, timestamp: Date.now(), type }; session.messageHistory.push(chatMessage); // Keep only last 100 messages if (session.messageHistory.length > 100) { session.messageHistory = session.messageHistory.slice(-100); } this.logger.debug('Message sent to workspace', { workspaceId, userId, type }); this.emit('messageSent', { workspaceId, message: chatMessage }); return true; } /** * Get workspace statistics */ getWorkspaceStats(workspaceId) { const workspace = this.workspaces.get(workspaceId); const session = this.activeSessions.get(workspaceId); if (!workspace) { return null; } const recentEvents = this.getRecentEvents(workspaceId, 1000); return { collaborators: workspace.collaborators.length, activeUsers: session ? session.participants.filter(p => p.isActive).length : 0, sharedRules: workspace.data.sharedRules.length, notes: workspace.data.collaborativeNotes.length, patterns: workspace.data.learningPatterns.length, totalActivity: recentEvents.length }; } /** * Export workspace data */ exportWorkspace(workspaceId) { const workspace = this.workspaces.get(workspaceId); return workspace ? { ...workspace } : null; } /** * Import workspace data */ importWorkspace(workspaceData) { try { // Validate workspace data if (!workspaceData.id || !workspaceData.name || !workspaceData.owner) { return false; } // Check if workspace already exists if (this.workspaces.has(workspaceData.id)) { return false; } this.workspaces.set(workspaceData.id, workspaceData); this.logger.info('Workspace imported', { workspaceId: workspaceData.id, name: workspaceData.name }); this.emit('workspaceImported', workspaceData); return true; } catch (error) { this.logger.error('Failed to import workspace', error); return false; } } /** * Check if user can edit in workspace */ canUserEdit(workspaceId, userId) { const workspace = this.workspaces.get(workspaceId); const session = this.activeSessions.get(workspaceId); if (!workspace?.settings.allowEditing) { return false; } if (!workspace.collaborators.includes(userId)) { return false; } if (session) { const participant = session.participants.find(p => p.userId === userId); return participant ? participant.role !== 'viewer' : false; } return true; } /** * Check for edit conflicts */ hasEditConflict(workspaceId, resourceId, userId) { const session = this.activeSessions.get(workspaceId); if (!session) { return false; } const lockId = `${workspaceId}_${resourceId}`; const lock = session.lockState.get(lockId); return lock ? lock.userId !== userId : false; } /** * Handle edit conflict */ handleEditConflict(workspaceId, resourceId, userId, proposedChanges) { const conflictId = `conflict_${workspaceId}_${resourceId}_${Date.now()}`; const conflict = { id: conflictId, resourceType: 'rule', resourceId, conflictType: 'edit_conflict', participants: [userId], proposedChanges: [{ userId, changes: proposedChanges, timestamp: Date.now() }] }; this.pendingConflicts.set(conflictId, conflict); this.logger.warn('Edit conflict detected', { workspaceId, resourceId, userId }); this.emit('conflictDetected', conflict); } /** * Record sync event */ recordSyncEvent(event) { const syncEvent = { id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now(), version: this.syncEvents.length + 1, ...event }; this.syncEvents.push(syncEvent); // Keep only recent events if (this.syncEvents.length > 10000) { this.syncEvents = this.syncEvents.slice(-5000); } this.emit('syncEvent', syncEvent); } /** * Start sync timer for real-time updates */ startSyncTimer() { this.syncInterval = setInterval(() => { this.performSync(); }, 5000); // Sync every 5 seconds } /** * Perform synchronization across active sessions */ performSync() { for (const [workspaceId, session] of this.activeSessions) { // Update participant activity status const now = Date.now(); for (const participant of session.participants) { participant.isActive = (now - participant.lastActivity) < 30000; // 30 seconds } // Auto-release old locks for (const [lockId, lock] of session.lockState) { if ((now - lock.timestamp) > 5 * 60 * 1000) { // 5 minutes session.lockState.delete(lockId); this.logger.debug('Auto-released expired lock', { workspaceId, lockId, userId: lock.userId }); } } } } /** * Start cleanup timer for old data */ startCleanupTimer() { this.cleanupInterval = setInterval(() => { this.performCleanup(); }, 60 * 60 * 1000); // Every hour } /** * Cleanup old data and inactive sessions */ performCleanup() { const now = Date.now(); const cutoff = now - (24 * 60 * 60 * 1000); // 24 hours // Remove old sync events const beforeCount = this.syncEvents.length; this.syncEvents = this.syncEvents.filter(event => event.timestamp >= cutoff); if (this.syncEvents.length < beforeCount) { this.logger.debug('Cleaned up old sync events', { removed: beforeCount - this.syncEvents.length, remaining: this.syncEvents.length }); } // Remove inactive sessions let removedSessions = 0; for (const [workspaceId, session] of this.activeSessions) { const hasActiveParticipants = session.participants.some(p => p.isActive); if (!hasActiveParticipants) { this.activeSessions.delete(workspaceId); removedSessions++; } } if (removedSessions > 0) { this.logger.debug('Cleaned up inactive sessions', { removed: removedSessions }); } } /** * Cleanup resources */ destroy() { if (this.syncInterval) { clearInterval(this.syncInterval); } if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.workspaces.clear(); this.activeSessions.clear(); this.syncEvents = []; this.pendingConflicts.clear(); this.removeAllListeners(); this.logger.info('Collaboration Service destroyed'); } } // Global collaboration service instance export const collaborationService = (logger) => new CollaborationService(logger); //# sourceMappingURL=collaboration-service.js.map