Files
michaelschiemer/resources/js/modules/livecomponent/OptimisticStateManager.js
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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;