mitlist/fe/src/composables/useConflictResolution.ts
mohamad 66daa19cd5
Some checks failed
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Failing after 1m17s
feat: Implement WebSocket enhancements and introduce new components for settlements
This commit includes several key updates and new features:

- Enhanced WebSocket functionality across various components, improving real-time communication and user experience.
- Introduced new components for managing settlements, including `SettlementCard.vue`, `SettlementForm.vue`, and `SuggestedSettlementsCard.vue`, to streamline financial interactions.
- Updated existing components and services to support the new settlement features, ensuring consistency and improved performance.
- Added advanced performance optimizations to enhance loading times and responsiveness throughout the application.

These changes aim to provide a more robust and user-friendly experience in managing financial settlements and real-time interactions.
2025-06-30 01:07:10 +02:00

286 lines
9.8 KiB
TypeScript

// fe/src/composables/useConflictResolution.ts
// Unified conflict resolution system for optimistic updates
import { ref, computed } from 'vue'
import type { ConflictResolution, OptimisticUpdate } from '@/types/shared'
interface ConflictState {
pendingConflicts: Map<string, ConflictResolution<any>>
optimisticUpdates: Map<string, OptimisticUpdate<any>>
resolutionStrategies: Map<string, 'local' | 'server' | 'merge' | 'manual'>
}
const state = ref<ConflictState>({
pendingConflicts: new Map(),
optimisticUpdates: new Map(),
resolutionStrategies: new Map()
})
export function useConflictResolution() {
// Computed properties
const hasConflicts = computed(() => state.value.pendingConflicts.size > 0)
const conflictCount = computed(() => state.value.pendingConflicts.size)
const pendingUpdates = computed(() => state.value.optimisticUpdates.size)
// Track an optimistic update
function trackOptimisticUpdate<T>(
entityType: string,
entityId: number,
operation: 'create' | 'update' | 'delete',
data: T
): string {
const updateId = `${entityType}_${entityId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
const update: OptimisticUpdate<T> = {
id: updateId,
entity_type: entityType,
entity_id: entityId,
operation,
data,
timestamp: new Date().toISOString(),
status: 'pending'
}
state.value.optimisticUpdates.set(updateId, update)
console.log('[ConflictResolution] Tracked optimistic update:', updateId)
return updateId
}
// Confirm an optimistic update succeeded
function confirmOptimisticUpdate(updateId: string, serverData?: any) {
const update = state.value.optimisticUpdates.get(updateId)
if (update) {
update.status = 'confirmed'
if (serverData) {
update.data = serverData
}
// Keep confirmed updates for a short time for debugging
setTimeout(() => {
state.value.optimisticUpdates.delete(updateId)
}, 5000)
console.log('[ConflictResolution] Confirmed optimistic update:', updateId)
}
}
// Mark an optimistic update as failed and create conflict if needed
function failOptimisticUpdate<T>(
updateId: string,
serverData: T,
serverVersion: number
): string | null {
const update = state.value.optimisticUpdates.get(updateId)
if (!update) return null
update.status = 'failed'
// Create conflict resolution entry
const conflictId = `conflict_${updateId}`
const conflict: ConflictResolution<T> = {
id: conflictId,
entity_type: update.entity_type,
entity_id: update.entity_id,
local_version: 0, // We don't track versions in optimistic updates
server_version: serverVersion,
local_data: update.data,
server_data: serverData,
resolution_strategy: getDefaultStrategy(update.entity_type)
}
state.value.pendingConflicts.set(conflictId, conflict)
console.log('[ConflictResolution] Created conflict:', conflictId)
return conflictId
}
// Get default resolution strategy for entity type
function getDefaultStrategy(entityType: string): 'local' | 'server' | 'merge' | 'manual' {
const strategies = state.value.resolutionStrategies.get(entityType)
if (strategies) return strategies
// Default strategies by entity type
switch (entityType) {
case 'expense':
case 'settlement':
return 'manual' // Financial data requires manual review
case 'chore':
return 'server' // Prefer server state for chores
case 'list':
case 'item':
return 'merge' // Try to merge list changes
default:
return 'server'
}
}
// Set resolution strategy for entity type
function setResolutionStrategy(entityType: string, strategy: 'local' | 'server' | 'merge' | 'manual') {
state.value.resolutionStrategies.set(entityType, strategy)
}
// Resolve a conflict with chosen strategy
function resolveConflict<T>(
conflictId: string,
strategy: 'local' | 'server' | 'merge' | 'manual',
mergedData?: T
): T | null {
const conflict = state.value.pendingConflicts.get(conflictId)
if (!conflict) return null
let resolvedData: T
switch (strategy) {
case 'local':
resolvedData = conflict.local_data
break
case 'server':
resolvedData = conflict.server_data
break
case 'merge':
resolvedData = mergeData(conflict.local_data, conflict.server_data)
break
case 'manual':
if (!mergedData) {
console.error('[ConflictResolution] Manual resolution requires merged data')
return null
}
resolvedData = mergedData
break
}
// Update conflict with resolution
conflict.resolution_strategy = strategy
conflict.resolved_data = resolvedData
conflict.resolved_at = new Date().toISOString()
// Remove from pending conflicts
state.value.pendingConflicts.delete(conflictId)
console.log('[ConflictResolution] Resolved conflict:', conflictId, 'with strategy:', strategy)
return resolvedData
}
// Smart merge algorithm for common data types
function mergeData<T>(localData: T, serverData: T): T {
if (typeof localData !== 'object' || typeof serverData !== 'object') {
return serverData // Fallback to server data for primitives
}
// For objects, merge non-conflicting properties
const merged = { ...serverData } as any
const local = localData as any
const server = serverData as any
for (const key in local) {
if (local.hasOwnProperty(key)) {
// Prefer local changes for certain fields
if (isUserEditableField(key)) {
merged[key] = local[key]
}
// For arrays, try to merge intelligently
else if (Array.isArray(local[key]) && Array.isArray(server[key])) {
merged[key] = mergeArrays(local[key], server[key])
}
// For dates, prefer the more recent one
else if (isDateField(key)) {
const localDate = new Date(local[key])
const serverDate = new Date(server[key])
merged[key] = localDate > serverDate ? local[key] : server[key]
}
}
}
return merged as T
}
// Check if field is typically user-editable
function isUserEditableField(fieldName: string): boolean {
const editableFields = [
'name', 'description', 'notes', 'title',
'quantity', 'price', 'category',
'priority', 'status',
'custom_interval_days'
]
return editableFields.includes(fieldName)
}
// Check if field represents a date
function isDateField(fieldName: string): boolean {
return fieldName.includes('_at') || fieldName.includes('date') || fieldName.includes('time')
}
// Merge arrays intelligently (basic implementation)
function mergeArrays<T>(localArray: T[], serverArray: T[]): T[] {
// Simple merge - combine arrays and remove duplicates based on id
const merged = [...serverArray]
localArray.forEach(localItem => {
const localItemAny = localItem as any
if (localItemAny.id) {
const existingIndex = merged.findIndex((item: any) => item.id === localItemAny.id)
if (existingIndex >= 0) {
// Replace with local version
merged[existingIndex] = localItem
} else {
// Add new local item
merged.push(localItem)
}
} else {
// No ID, just add if not duplicate
if (!merged.some(item => JSON.stringify(item) === JSON.stringify(localItem))) {
merged.push(localItem)
}
}
})
return merged
}
// Get all pending conflicts
function getPendingConflicts() {
return Array.from(state.value.pendingConflicts.values())
}
// Get specific conflict
function getConflict(conflictId: string) {
return state.value.pendingConflicts.get(conflictId)
}
// Clear all conflicts (for testing or reset)
function clearConflicts() {
state.value.pendingConflicts.clear()
state.value.optimisticUpdates.clear()
}
// Get statistics
function getStats() {
return {
pendingConflicts: state.value.pendingConflicts.size,
optimisticUpdates: state.value.optimisticUpdates.size,
confirmedUpdates: Array.from(state.value.optimisticUpdates.values())
.filter(u => u.status === 'confirmed').length,
failedUpdates: Array.from(state.value.optimisticUpdates.values())
.filter(u => u.status === 'failed').length
}
}
return {
// State
hasConflicts,
conflictCount,
pendingUpdates,
// Methods
trackOptimisticUpdate,
confirmOptimisticUpdate,
failOptimisticUpdate,
resolveConflict,
setResolutionStrategy,
getPendingConflicts,
getConflict,
clearConflicts,
getStats
}
}