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
JavaScript
/**
* 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;