rezilient.js
Version:
Rezilient.js - Revolutionary offline-first framework with AI-awareness, principle-driven development, carbon-conscious computing, and self-healing capabilities
866 lines (747 loc) • 24.1 kB
JavaScript
// src/sync/SyncEngine.js
import { get, set, del } from 'idb-keyval';
import { PersistentStore } from '../data/PersistentStore.js';
import { AetherStore } from '../data/AetherStore.js';
import { CacheManager } from '../cache/CacheManager.js';
import { CarbonAwareScheduler } from '../scheduler/CarbonAwareScheduler.js';
const MUTATION_QUEUE_KEY = 'aether-mutation-queue';
// Sync status constants
export const SYNC_STATUS = {
IDLE: 'idle',
SYNCING: 'syncing',
ERROR: 'error',
SYNCED: 'synced',
OFFLINE: 'offline'
};
// Sync event types
export const SYNC_EVENTS = {
STATUS_CHANGE: 'sync-status-change',
PROGRESS_UPDATE: 'sync-progress-update',
MUTATION_SYNCED: 'mutation-synced',
SYNC_ERROR: 'sync-error',
QUEUE_UPDATED: 'queue-updated'
};
/**
* @class SyncEngine
* Enhanced sync engine with real-time status updates, progress tracking,
* and intelligent error handling for the Aether.js Resilient-First paradigm.
*/
export class SyncEngine {
/**
* @param {Object} [options]
* @param {'LastWriteWins'|'ServerWins'|function} [options.conflictStrategy] - Conflict resolution strategy.
* @param {number} [options.retryAttempts=3] - Number of retry attempts for failed mutations.
* @param {number} [options.retryDelay=1000] - Delay between retry attempts in milliseconds.
* @param {boolean} [options.enableProgressTracking=true] - Enable detailed progress tracking.
* @param {boolean} [options.enableAdvancedCaching=true] - Enable advanced caching strategies.
* @param {boolean} [options.enableCarbonAware=true] - Enable carbon-aware scheduling.
*/
constructor(options = {}) {
this.queue = new PersistentStore(MUTATION_QUEUE_KEY, []);
this.isSyncing = false;
this.conflictStrategy = options.conflictStrategy || 'LastWriteWins';
this.retryAttempts = options.retryAttempts || 3;
this.retryDelay = options.retryDelay || 1000;
this.enableProgressTracking = options.enableProgressTracking !== false;
this.enableAdvancedCaching = options.enableAdvancedCaching !== false;
this.enableCarbonAware = options.enableCarbonAware !== false;
// Enhanced sync state management
this.syncState = new AetherStore({
status: SYNC_STATUS.IDLE,
progress: { current: 0, total: 0, percentage: 0 },
pending: 0,
error: null,
lastSync: null,
retryCount: 0
});
// Event listeners for sync state changes
this.eventListeners = new Map();
// Network state tracking
this.isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
this.setupEventListeners();
this.initializeSyncState();
this.initializeAdvancedFeatures();
}
/**
* Initialize advanced features (caching and carbon-aware scheduling)
* @private
*/
async initializeAdvancedFeatures() {
// Initialize cache manager
if (this.enableAdvancedCaching) {
this.cacheManager = new CacheManager({
enablePredictiveCaching: true,
enableCarbonAware: this.enableCarbonAware
});
}
// Initialize carbon-aware scheduler
if (this.enableCarbonAware) {
this.carbonScheduler = new CarbonAwareScheduler({
enableCarbonAwareness: true
});
}
}
/**
* Setup event listeners for network and sync events
* @private
*/
setupEventListeners() {
// Only setup event listeners if we're in a browser environment with window object
if (typeof window !== 'undefined' && window && window.addEventListener) {
window.addEventListener('online', () => {
this.isOnline = true;
this.updateSyncStatus(SYNC_STATUS.IDLE);
this.processQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
this.updateSyncStatus(SYNC_STATUS.OFFLINE);
});
}
}
/**
* Initialize sync state based on current conditions
* @private
*/
async initializeSyncState() {
const queueLength = (await this.queue.get()).length;
this.updateSyncState({
pending: queueLength,
status: this.isOnline ? SYNC_STATUS.IDLE : SYNC_STATUS.OFFLINE
});
}
/**
* Enhanced sync state management methods
*/
/**
* Update sync state and notify listeners
* @private
*/
updateSyncState(updates) {
this.syncState.update(current => ({ ...current, ...updates }));
this.emitEvent(SYNC_EVENTS.STATUS_CHANGE, this.syncState.get());
}
/**
* Update sync status specifically
* @private
*/
updateSyncStatus(status) {
this.updateSyncState({ status });
}
/**
* Update sync progress
* @private
*/
updateProgress(current, total) {
if (!this.enableProgressTracking) return;
const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
const progress = { current, total, percentage };
this.updateSyncState({ progress });
this.emitEvent(SYNC_EVENTS.PROGRESS_UPDATE, progress);
}
/**
* Emit sync events to listeners
* @private
*/
emitEvent(eventType, data) {
const listeners = this.eventListeners.get(eventType) || [];
listeners.forEach(listener => {
try {
listener(data);
} catch (error) {
console.error(`Error in sync event listener for ${eventType}:`, error);
}
});
}
/**
* Subscribe to sync events
* @param {string} eventType - Event type to listen for
* @param {function} listener - Event listener function
* @returns {function} Unsubscribe function
*/
addEventListener(eventType, listener) {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType).push(listener);
// Return unsubscribe function
return () => {
const listeners = this.eventListeners.get(eventType) || [];
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
/**
* Get current sync state
* @returns {object} Current sync state
*/
getSyncState() {
return this.syncState.get();
}
/**
* Get current sync state (alias for getSyncState)
* @returns {object} Current sync state
*/
getState() {
return this.getSyncState();
}
/**
* Subscribe to sync state changes
* @param {function} callback - Callback function
* @returns {function} Unsubscribe function
*/
subscribeSyncState(callback) {
return this.syncState.subscribe(callback);
}
/**
* Adds a mutation to the queue with enhanced tracking.
* @param {object} mutation - The mutation object to add.
* It should contain all information needed to perform the action later.
* e.g., { type: 'ADD_ITEM', payload: { id: 1, text: 'New item' } }
*/
async addMutation(mutation) {
// Add metadata to mutation
const enhancedMutation = {
...mutation,
id: mutation.id || this.generateMutationId(),
timestamp: mutation.timestamp || Date.now(),
retryCount: 0,
status: 'pending'
};
const currentQueue = (await this.queue.get()) || [];
const newQueue = [...currentQueue, enhancedMutation];
await this.queue.set(newQueue);
// Update sync state
this.updateSyncState({ pending: newQueue.length });
this.emitEvent(SYNC_EVENTS.QUEUE_UPDATED, {
action: 'added',
mutation: enhancedMutation,
queueLength: newQueue.length
});
// Auto-process if online and not already syncing
if (this.isOnline && !this.isSyncing) {
setTimeout(() => this.processQueue(), 100);
}
}
/**
* Generate unique mutation ID
* @private
*/
generateMutationId() {
return `mutation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Enhanced queue processing with progress tracking, error recovery, and carbon-aware scheduling.
* This method should be called when the application comes online.
*/
async processQueue(options = {}) {
// Guard clauses
if (this.isSyncing) {
console.log('Sync already in progress, skipping...');
return;
}
if (!this.isOnline) {
console.log('Device is offline, cannot sync');
this.updateSyncStatus(SYNC_STATUS.OFFLINE);
return;
}
const mutations = (await this.queue.get()) || [];
if (mutations.length === 0) {
this.updateSyncState({
status: SYNC_STATUS.SYNCED,
pending: 0,
error: null
});
return;
}
// Use carbon-aware scheduling if enabled
if (this.enableCarbonAware && this.carbonScheduler && !options.force) {
return this.processQueueWithCarbonAwareness(mutations, options);
}
// Start sync process
this.isSyncing = true;
this.updateSyncState({
status: SYNC_STATUS.SYNCING,
error: null,
retryCount: 0
});
const totalMutations = mutations.length;
let processedCount = 0;
const failedMutations = [];
const successfulMutations = [];
this.updateProgress(0, totalMutations);
// Process mutations with enhanced error handling
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
try {
// Update progress
this.updateProgress(i, totalMutations);
// Process single mutation with retry logic
const result = await this.processSingleMutation(mutation);
if (result.success) {
successfulMutations.push(mutation);
this.emitEvent(SYNC_EVENTS.MUTATION_SYNCED, {
mutation,
result: result.data
});
} else {
failedMutations.push({
mutation,
error: result.error,
retryCount: mutation.retryCount || 0
});
}
processedCount++;
} catch (error) {
console.error('Failed to sync mutation:', mutation, error);
failedMutations.push({
mutation,
error,
retryCount: mutation.retryCount || 0
});
}
}
// Update final progress
this.updateProgress(totalMutations, totalMutations);
// Handle results
await this.handleSyncResults(successfulMutations, failedMutations);
this.isSyncing = false;
}
/**
* Process queue with carbon-aware scheduling
* @private
*/
async processQueueWithCarbonAwareness(mutations, options = {}) {
const priority = options.priority || 'normal';
// Group mutations by type for batch processing
const mutationGroups = this.groupMutationsByType(mutations);
for (const [type, groupMutations] of mutationGroups) {
const batchTask = {
type: 'sync-batch',
data: groupMutations,
execute: async (mutations) => {
return this.processMutationBatch(mutations);
},
estimatedDuration: groupMutations.length * 500 // Estimate 500ms per mutation
};
// Schedule with carbon awareness
await this.carbonScheduler.scheduleTask(batchTask, priority, {
carbonAware: true,
networkAware: true,
batteryAware: true
});
}
}
/**
* Group mutations by type for efficient batch processing
* @private
*/
groupMutationsByType(mutations) {
const groups = new Map();
for (const mutation of mutations) {
const type = mutation.type || 'default';
if (!groups.has(type)) {
groups.set(type, []);
}
groups.get(type).push(mutation);
}
return groups;
}
/**
* Process a batch of mutations
* @private
*/
async processMutationBatch(mutations) {
const results = [];
for (const mutation of mutations) {
try {
const result = await this.processSingleMutation(mutation);
results.push({ mutation, result });
} catch (error) {
results.push({ mutation, error });
}
}
// Update queue after batch processing
await this.updateQueueAfterBatch(results);
return results;
}
/**
* Update queue after batch processing
* @private
*/
async updateQueueAfterBatch(results) {
const currentQueue = await this.queue.get();
const successfulMutations = results
.filter(r => r.result && r.result.success)
.map(r => r.mutation);
// Remove successful mutations from queue
const updatedQueue = currentQueue.filter(queueMutation =>
!successfulMutations.some(successful =>
successful.id === queueMutation.id
)
);
await this.queue.set(updatedQueue);
// Update sync state
this.updateSyncState({
pending: updatedQueue.length,
status: updatedQueue.length === 0 ? SYNC_STATUS.SYNCED : SYNC_STATUS.IDLE
});
}
/**
* Process a single mutation with retry logic
* @private
*/
async processSingleMutation(mutation) {
let lastError = null;
const maxRetries = mutation.retryCount < this.retryAttempts ?
this.retryAttempts - mutation.retryCount : 0;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Fetch server state for conflict resolution
const serverState = await this.fetchServerState(mutation);
let resolvedMutation = mutation;
// Conflict resolution
if (serverState && this.hasConflict(mutation, serverState)) {
if (typeof this.conflictStrategy === 'function') {
resolvedMutation = this.conflictStrategy(mutation, serverState);
if (!resolvedMutation) {
// Custom resolver decided to skip this mutation
return { success: true, data: { skipped: true, reason: 'conflict_resolved' } };
}
} else if (this.conflictStrategy === 'ServerWins') {
// Discard local mutation
return { success: true, data: { skipped: true, reason: 'server_wins' } };
} else if (this.conflictStrategy === 'LastWriteWins') {
// Default: apply local mutation
}
}
// Attempt to sync the mutation
const result = await this.syncMutation(resolvedMutation);
return { success: true, data: result };
} catch (error) {
lastError = error;
// If this isn't the last attempt, wait before retrying
if (attempt < maxRetries) {
const delay = this.retryDelay * Math.pow(2, attempt); // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// All retries failed
return {
success: false,
error: lastError,
retriesExhausted: true
};
}
/**
* Handle sync results and update queue
* @private
*/
async handleSyncResults(successfulMutations, failedMutations) {
const currentQueue = await this.queue.get();
// Remove successful mutations from queue
const remainingQueue = currentQueue.filter(queueMutation =>
!successfulMutations.some(successful =>
successful.id === queueMutation.id
)
);
// Update failed mutations with retry count
const updatedFailedMutations = failedMutations.map(failed => ({
...failed.mutation,
retryCount: (failed.mutation.retryCount || 0) + 1,
lastError: failed.error.message,
lastAttempt: Date.now()
}));
// Keep failed mutations that haven't exceeded retry limit
const retriableMutations = updatedFailedMutations.filter(mutation =>
mutation.retryCount < this.retryAttempts
);
// Final queue with only retriable mutations
const finalQueue = remainingQueue.map(queueMutation => {
const failedUpdate = retriableMutations.find(failed =>
failed.id === queueMutation.id
);
return failedUpdate || queueMutation;
});
await this.queue.set(finalQueue);
// Update sync state
const hasErrors = failedMutations.length > 0;
const allRetryExhausted = failedMutations.every(f => f.retriesExhausted);
this.updateSyncState({
status: hasErrors ?
(allRetryExhausted ? SYNC_STATUS.ERROR : SYNC_STATUS.IDLE) :
SYNC_STATUS.SYNCED,
pending: finalQueue.length,
error: hasErrors ? failedMutations[0].error : null,
lastSync: Date.now()
});
// Emit error events for failed mutations
if (hasErrors) {
failedMutations.forEach(failed => {
this.emitEvent(SYNC_EVENTS.SYNC_ERROR, {
mutation: failed.mutation,
error: failed.error,
retryCount: failed.mutation.retryCount || 0,
retriesExhausted: failed.retriesExhausted
});
});
}
// Schedule retry for retriable mutations
if (retriableMutations.length > 0 && this.isOnline) {
setTimeout(() => {
if (!this.isSyncing) {
this.processQueue();
}
}, this.retryDelay * 2);
}
}
/**
* Simulate fetching the latest server state for a resource.
* In a real implementation, this would fetch from the server.
*/
async fetchServerState(mutation) {
// Placeholder: return null to indicate no conflict by default
return null;
}
/**
* Determines if there is a conflict between the mutation and server state.
* @private
*/
hasConflict(mutation, serverState) {
// Placeholder: always returns false (no conflict)
// In a real implementation, compare mutation and serverState
return false;
}
/**
* Simulates syncing a single mutation to a server.
* @param {object} mutation - The mutation to sync.
* @private
*/
async syncMutation(mutation) {
// In a real implementation, this would be a fetch() call to your API.
console.log('Syncing mutation:', mutation);
return new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
}
/**
* Enhanced utility methods
*/
/**
* Returns the current mutation queue with metadata.
* @returns {Promise<Array>} The current queue.
*/
async getQueue() {
const queue = await this.queue.get();
return queue.map(mutation => ({
...mutation,
age: Date.now() - (mutation.timestamp || 0),
canRetry: (mutation.retryCount || 0) < this.retryAttempts
}));
}
/**
* Get queue statistics
* @returns {Promise<object>} Queue statistics
*/
async getQueueStats() {
const queue = await this.getQueue();
const now = Date.now();
return {
total: queue.length,
pending: queue.filter(m => m.status === 'pending').length,
failed: queue.filter(m => (m.retryCount || 0) >= this.retryAttempts).length,
retriable: queue.filter(m => m.canRetry).length,
oldestMutation: queue.length > 0 ?
Math.max(...queue.map(m => now - (m.timestamp || 0))) : 0,
averageAge: queue.length > 0 ?
queue.reduce((sum, m) => sum + (now - (m.timestamp || 0)), 0) / queue.length : 0
};
}
/**
* Clear all mutations from queue (use with caution)
* @returns {Promise<void>}
*/
async clearQueue() {
await this.queue.set([]);
this.updateSyncState({
pending: 0,
status: SYNC_STATUS.IDLE,
error: null
});
this.emitEvent(SYNC_EVENTS.QUEUE_UPDATED, {
action: 'cleared',
queueLength: 0
});
}
/**
* Remove specific mutation from queue
* @param {string} mutationId - ID of mutation to remove
* @returns {Promise<boolean>} True if mutation was found and removed
*/
async removeMutation(mutationId) {
const currentQueue = await this.queue.get();
const filteredQueue = currentQueue.filter(m => m.id !== mutationId);
if (filteredQueue.length !== currentQueue.length) {
await this.queue.set(filteredQueue);
this.updateSyncState({ pending: filteredQueue.length });
this.emitEvent(SYNC_EVENTS.QUEUE_UPDATED, {
action: 'removed',
mutationId,
queueLength: filteredQueue.length
});
return true;
}
return false;
}
/**
* Force sync of specific mutation
* @param {string} mutationId - ID of mutation to sync
* @returns {Promise<boolean>} True if mutation was found and synced
*/
async forceSyncMutation(mutationId) {
const currentQueue = await this.queue.get();
const mutation = currentQueue.find(m => m.id === mutationId);
if (!mutation) {
return false;
}
if (!this.isOnline) {
throw new Error('Cannot force sync while offline');
}
try {
const result = await this.processSingleMutation(mutation);
if (result.success) {
await this.removeMutation(mutationId);
this.emitEvent(SYNC_EVENTS.MUTATION_SYNCED, {
mutation,
result: result.data,
forced: true
});
return true;
} else {
throw result.error;
}
} catch (error) {
this.emitEvent(SYNC_EVENTS.SYNC_ERROR, {
mutation,
error,
forced: true
});
throw error;
}
}
/**
* Check if sync engine is healthy
* @returns {Promise<object>} Health status
*/
async getHealthStatus() {
const stats = await this.getQueueStats();
const state = this.getSyncState();
const health = {
status: 'healthy',
issues: [],
recommendations: []
};
// Check for issues
if (!this.isOnline) {
health.status = 'offline';
health.issues.push('Device is offline');
}
if (stats.failed > 0) {
health.status = 'degraded';
health.issues.push(`${stats.failed} mutations have failed permanently`);
health.recommendations.push('Review failed mutations and consider manual intervention');
}
if (stats.oldestMutation > 24 * 60 * 60 * 1000) { // 24 hours
health.status = 'degraded';
health.issues.push('Mutations older than 24 hours in queue');
health.recommendations.push('Check network connectivity and server availability');
}
if (state.status === SYNC_STATUS.ERROR) {
health.status = 'error';
health.issues.push('Sync engine is in error state');
health.recommendations.push('Check error details and retry sync');
}
return {
...health,
stats,
state,
timestamp: Date.now()
};
}
/**
* Get advanced features statistics
*/
getAdvancedStats() {
const stats = {
caching: null,
carbonScheduling: null,
features: {
advancedCaching: this.enableAdvancedCaching,
carbonAware: this.enableCarbonAware
}
};
if (this.cacheManager) {
stats.caching = this.cacheManager.getStats();
}
if (this.carbonScheduler) {
stats.carbonScheduling = this.carbonScheduler.getStats();
}
return stats;
}
/**
* Get carbon data if carbon-aware scheduling is enabled
*/
getCarbonData() {
if (this.carbonScheduler) {
return this.carbonScheduler.getCarbonData();
}
return null;
}
/**
* Force immediate sync (bypass carbon-aware scheduling)
*/
async forceSync() {
return this.processQueue({ force: true });
}
/**
* Schedule predictive caching for a URL
*/
async schedulePredictiveCache(currentUrl) {
if (this.cacheManager) {
return this.cacheManager.predictivePreCache(currentUrl);
}
}
/**
* Invalidate cache with smart strategies
*/
async invalidateCache(pattern, options = {}) {
if (this.cacheManager) {
return this.cacheManager.invalidateCache(pattern, options);
}
return { invalidated: 0, preserved: 0, errors: [] };
}
/**
* Destroy sync engine and clean up resources
*/
destroy() {
// Clear all event listeners
this.eventListeners.clear();
// Cleanup advanced features
if (this.cacheManager) {
// CacheManager cleanup would go here
}
if (this.carbonScheduler) {
// CarbonScheduler cleanup would go here
}
// Remove window event listeners
if (typeof window !== 'undefined') {
// Note: We can't remove specific listeners without references
// This is a limitation of the current implementation
console.warn('Window event listeners for SyncEngine cannot be automatically removed');
}
// Reset state
this.isSyncing = false;
this.updateSyncStatus(SYNC_STATUS.IDLE);
}
}