- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
446 lines
14 KiB
JavaScript
446 lines
14 KiB
JavaScript
/**
|
|
* Optimistic State Manager
|
|
*
|
|
* Manages optimistic UI updates with version-based conflict resolution.
|
|
* Implements the "Optimistic UI" pattern where client immediately updates
|
|
* the UI before server confirms, then rolls back on conflicts.
|
|
*
|
|
* Key Features:
|
|
* - Immediate UI feedback (no loading spinners)
|
|
* - Version-based optimistic concurrency control
|
|
* - Automatic conflict detection and rollback
|
|
* - Pending operations queue with retry
|
|
* - User-friendly conflict resolution
|
|
*
|
|
* Protocol:
|
|
* 1. Client updates UI immediately (optimistic)
|
|
* 2. Client sends update to server with version
|
|
* 3. Server checks version and either:
|
|
* a) Accepts: version matches, returns new state with version++
|
|
* b) Rejects: version conflict, returns current server state
|
|
* 4. Client resolves:
|
|
* a) On accept: commit optimistic state
|
|
* b) On conflict: rollback to server state, show conflict UI
|
|
*/
|
|
|
|
export class OptimisticStateManager {
|
|
constructor() {
|
|
/**
|
|
* Pending operations queue
|
|
* Map<componentId, PendingOperation[]>
|
|
*/
|
|
this.pendingOperations = new Map();
|
|
|
|
/**
|
|
* Rollback snapshots
|
|
* Map<componentId, StateSnapshot>
|
|
*/
|
|
this.snapshots = new Map();
|
|
|
|
/**
|
|
* Conflict callbacks
|
|
* Map<componentId, ConflictCallback>
|
|
*/
|
|
this.conflictHandlers = new Map();
|
|
|
|
/**
|
|
* Optimistic operation counter
|
|
*/
|
|
this.operationIdCounter = 0;
|
|
}
|
|
|
|
/**
|
|
* Apply optimistic update
|
|
*
|
|
* Updates component state immediately without waiting for server confirmation.
|
|
* Creates snapshot for potential rollback.
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {Object} currentState - Current component state (with version)
|
|
* @param {Function} optimisticUpdate - Function that returns optimistically updated state
|
|
* @param {Object} metadata - Operation metadata (action, params)
|
|
* @returns {Object} Optimistic state with incremented version
|
|
*/
|
|
applyOptimisticUpdate(componentId, currentState, optimisticUpdate, metadata = {}) {
|
|
// Create snapshot before applying optimistic update
|
|
if (!this.snapshots.has(componentId)) {
|
|
this.createSnapshot(componentId, currentState);
|
|
}
|
|
|
|
// Extract version from state
|
|
const currentVersion = currentState.version || 1;
|
|
|
|
// Apply optimistic update
|
|
const optimisticState = optimisticUpdate(currentState);
|
|
|
|
// Increment version optimistically
|
|
const newState = {
|
|
...optimisticState,
|
|
version: currentVersion + 1
|
|
};
|
|
|
|
// Create pending operation
|
|
const operation = {
|
|
id: this.generateOperationId(),
|
|
componentId,
|
|
metadata,
|
|
expectedVersion: currentVersion,
|
|
optimisticState: newState,
|
|
timestamp: Date.now(),
|
|
status: 'pending' // pending, confirmed, failed
|
|
};
|
|
|
|
// Add to pending operations queue
|
|
this.addPendingOperation(componentId, operation);
|
|
|
|
console.log(`[OptimisticUI] Applied optimistic update for ${componentId}`, {
|
|
version: `${currentVersion} → ${currentVersion + 1}`,
|
|
operationId: operation.id,
|
|
pendingCount: this.getPendingOperationsCount(componentId)
|
|
});
|
|
|
|
return newState;
|
|
}
|
|
|
|
/**
|
|
* Confirm operation success
|
|
*
|
|
* Called when server successfully processes the operation.
|
|
* Commits the optimistic state and clears snapshot if no more pending operations.
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {string} operationId - Operation ID to confirm
|
|
* @param {Object} serverState - Server-confirmed state (may differ from optimistic)
|
|
* @returns {Object} Final state (server state or optimistic if versions match)
|
|
*/
|
|
confirmOperation(componentId, operationId, serverState) {
|
|
const operation = this.getPendingOperation(componentId, operationId);
|
|
|
|
if (!operation) {
|
|
console.warn(`[OptimisticUI] Cannot confirm unknown operation: ${operationId}`);
|
|
return serverState;
|
|
}
|
|
|
|
// Mark operation as confirmed
|
|
operation.status = 'confirmed';
|
|
|
|
// Remove from pending queue
|
|
this.removePendingOperation(componentId, operationId);
|
|
|
|
console.log(`[OptimisticUI] Confirmed operation ${operationId} for ${componentId}`, {
|
|
pendingCount: this.getPendingOperationsCount(componentId)
|
|
});
|
|
|
|
// If no more pending operations, clear snapshot
|
|
if (!this.hasPendingOperations(componentId)) {
|
|
this.clearSnapshot(componentId);
|
|
}
|
|
|
|
return serverState;
|
|
}
|
|
|
|
/**
|
|
* Handle version conflict
|
|
*
|
|
* Called when server rejects operation due to version mismatch.
|
|
* Rolls back to snapshot and applies server state.
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {string} operationId - Failed operation ID
|
|
* @param {Object} serverState - Current server state
|
|
* @param {Object} conflict - Conflict information
|
|
* @returns {Object} Resolution result with rollback state and user notification
|
|
*/
|
|
handleConflict(componentId, operationId, serverState, conflict = {}) {
|
|
const operation = this.getPendingOperation(componentId, operationId);
|
|
|
|
if (!operation) {
|
|
console.warn(`[OptimisticUI] Cannot handle conflict for unknown operation: ${operationId}`);
|
|
return { state: serverState, notification: null };
|
|
}
|
|
|
|
// Mark operation as failed
|
|
operation.status = 'failed';
|
|
|
|
// Get snapshot for rollback
|
|
const snapshot = this.snapshots.get(componentId);
|
|
|
|
console.warn(`[OptimisticUI] Version conflict detected for ${componentId}`, {
|
|
operationId,
|
|
expectedVersion: operation.expectedVersion,
|
|
serverVersion: serverState.version,
|
|
metadata: operation.metadata,
|
|
pendingOperationsCount: this.getPendingOperationsCount(componentId)
|
|
});
|
|
|
|
// Clear all pending operations (cascade rollback)
|
|
this.clearPendingOperations(componentId);
|
|
|
|
// Clear snapshot
|
|
this.clearSnapshot(componentId);
|
|
|
|
// Call conflict handler if registered
|
|
const conflictHandler = this.conflictHandlers.get(componentId);
|
|
if (conflictHandler) {
|
|
conflictHandler({
|
|
operation,
|
|
serverState,
|
|
snapshotState: snapshot?.state,
|
|
conflict
|
|
});
|
|
}
|
|
|
|
// Create user notification
|
|
const notification = {
|
|
type: 'conflict',
|
|
title: 'Update Conflict',
|
|
message: 'Your changes conflicted with another update. The latest version has been loaded.',
|
|
action: operation.metadata.action || 'unknown',
|
|
canRetry: true,
|
|
operation
|
|
};
|
|
|
|
return {
|
|
state: serverState,
|
|
notification
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create state snapshot for rollback
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {Object} state - Current state to snapshot
|
|
*/
|
|
createSnapshot(componentId, state) {
|
|
this.snapshots.set(componentId, {
|
|
state: JSON.parse(JSON.stringify(state)), // Deep clone
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
console.log(`[OptimisticUI] Created snapshot for ${componentId}`, {
|
|
version: state.version
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear snapshot after all operations confirmed
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
*/
|
|
clearSnapshot(componentId) {
|
|
const snapshot = this.snapshots.get(componentId);
|
|
|
|
if (snapshot) {
|
|
this.snapshots.delete(componentId);
|
|
console.log(`[OptimisticUI] Cleared snapshot for ${componentId}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get rollback snapshot
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @returns {Object|null} Snapshot or null if none exists
|
|
*/
|
|
getSnapshot(componentId) {
|
|
const snapshot = this.snapshots.get(componentId);
|
|
return snapshot ? snapshot.state : null;
|
|
}
|
|
|
|
/**
|
|
* Add pending operation
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {Object} operation - Operation object
|
|
*/
|
|
addPendingOperation(componentId, operation) {
|
|
if (!this.pendingOperations.has(componentId)) {
|
|
this.pendingOperations.set(componentId, []);
|
|
}
|
|
|
|
this.pendingOperations.get(componentId).push(operation);
|
|
}
|
|
|
|
/**
|
|
* Remove pending operation
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {string} operationId - Operation ID to remove
|
|
*/
|
|
removePendingOperation(componentId, operationId) {
|
|
const operations = this.pendingOperations.get(componentId);
|
|
|
|
if (!operations) {
|
|
return;
|
|
}
|
|
|
|
const index = operations.findIndex(op => op.id === operationId);
|
|
|
|
if (index !== -1) {
|
|
operations.splice(index, 1);
|
|
}
|
|
|
|
// Clean up empty arrays
|
|
if (operations.length === 0) {
|
|
this.pendingOperations.delete(componentId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get specific pending operation
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {string} operationId - Operation ID
|
|
* @returns {Object|null} Operation or null if not found
|
|
*/
|
|
getPendingOperation(componentId, operationId) {
|
|
const operations = this.pendingOperations.get(componentId);
|
|
|
|
if (!operations) {
|
|
return null;
|
|
}
|
|
|
|
return operations.find(op => op.id === operationId) || null;
|
|
}
|
|
|
|
/**
|
|
* Get all pending operations for component
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @returns {Array<Object>} Array of pending operations
|
|
*/
|
|
getPendingOperations(componentId) {
|
|
return this.pendingOperations.get(componentId) || [];
|
|
}
|
|
|
|
/**
|
|
* Get pending operations count
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @returns {number} Number of pending operations
|
|
*/
|
|
getPendingOperationsCount(componentId) {
|
|
const operations = this.pendingOperations.get(componentId);
|
|
return operations ? operations.length : 0;
|
|
}
|
|
|
|
/**
|
|
* Check if component has pending operations
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @returns {boolean} True if has pending operations
|
|
*/
|
|
hasPendingOperations(componentId) {
|
|
return this.getPendingOperationsCount(componentId) > 0;
|
|
}
|
|
|
|
/**
|
|
* Clear all pending operations for component
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
*/
|
|
clearPendingOperations(componentId) {
|
|
this.pendingOperations.delete(componentId);
|
|
}
|
|
|
|
/**
|
|
* Register conflict handler
|
|
*
|
|
* Allows components to provide custom conflict resolution UI.
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {Function} handler - Conflict handler callback
|
|
*/
|
|
registerConflictHandler(componentId, handler) {
|
|
this.conflictHandlers.set(componentId, handler);
|
|
}
|
|
|
|
/**
|
|
* Unregister conflict handler
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
*/
|
|
unregisterConflictHandler(componentId) {
|
|
this.conflictHandlers.delete(componentId);
|
|
}
|
|
|
|
/**
|
|
* Generate unique operation ID
|
|
*
|
|
* @returns {string} Unique operation ID
|
|
*/
|
|
generateOperationId() {
|
|
return `op-${++this.operationIdCounter}-${Date.now()}`;
|
|
}
|
|
|
|
/**
|
|
* Retry failed operation
|
|
*
|
|
* Allows user to retry operation after conflict resolution.
|
|
*
|
|
* @param {string} componentId - Component identifier
|
|
* @param {Object} operation - Failed operation to retry
|
|
* @param {Function} retryCallback - Callback to execute retry
|
|
* @returns {Promise} Retry promise
|
|
*/
|
|
async retryOperation(componentId, operation, retryCallback) {
|
|
console.log(`[OptimisticUI] Retrying operation ${operation.id} for ${componentId}`);
|
|
|
|
try {
|
|
// Execute retry callback
|
|
const result = await retryCallback(operation.metadata);
|
|
|
|
console.log(`[OptimisticUI] Retry succeeded for ${operation.id}`);
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
console.error(`[OptimisticUI] Retry failed for ${operation.id}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get debugging stats
|
|
*
|
|
* @returns {Object} Statistics about optimistic operations
|
|
*/
|
|
getStats() {
|
|
const stats = {
|
|
total_components: this.pendingOperations.size,
|
|
total_pending_operations: 0,
|
|
total_snapshots: this.snapshots.size,
|
|
components: {}
|
|
};
|
|
|
|
this.pendingOperations.forEach((operations, componentId) => {
|
|
stats.total_pending_operations += operations.length;
|
|
stats.components[componentId] = {
|
|
pending: operations.length,
|
|
has_snapshot: this.snapshots.has(componentId)
|
|
};
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Clear all state (for testing/debugging)
|
|
*/
|
|
clear() {
|
|
this.pendingOperations.clear();
|
|
this.snapshots.clear();
|
|
this.conflictHandlers.clear();
|
|
this.operationIdCounter = 0;
|
|
|
|
console.log('[OptimisticUI] Cleared all state');
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
export const optimisticStateManager = new OptimisticStateManager();
|
|
|
|
// Export for testing and direct use
|
|
export default optimisticStateManager;
|