Last active
April 26, 2025 14:10
-
-
Save MikeTeddyOmondi/f8d358a2cd5cd87ee3d806af77612be9 to your computer and use it in GitHub Desktop.
Sync engine - JavaScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Sync Engine [JavaScript]</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 20px; | |
| } | |
| #syncStatus { | |
| margin-top: 10px; | |
| font-weight: bold; | |
| color: #4a90e2; | |
| } | |
| #log { | |
| margin-top: 10px; | |
| padding: 10px; | |
| border: 1px solid #ccc; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| input, | |
| button { | |
| font-size: 16px; | |
| padding: 8px; | |
| } | |
| .online-indicator { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 5px; | |
| } | |
| .online { | |
| background-color: #4caf50; | |
| } | |
| .offline { | |
| background-color: #f44336; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Sync Engine [JavaScript]</h1> | |
| <div> | |
| <span class="online-indicator" id="networkIndicator"></span> | |
| <span id="networkStatus">Checking connection...</span> | |
| </div> | |
| <p>Enter data to sync:</p> | |
| <form id="syncForm"> | |
| <input type="text" id="dataInput" placeholder="Enter data" /> | |
| <button type="submit">🔃 Sync</button> | |
| </form> | |
| <div id="syncStatus">Initializing sync engine...</div> | |
| <div id="log"></div> | |
| <script> | |
| // --- IndexedDB Setup --- | |
| const DB_NAME = "SyncEngineDB"; | |
| const DB_VERSION = 1; | |
| const STORE_NAME = "pendingSync"; | |
| let db; | |
| function openDatabase() { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open(DB_NAME, DB_VERSION); | |
| request.onerror = (event) => { | |
| log("IndexedDB error: " + event.target.errorCode); | |
| reject("IndexedDB error: " + event.target.errorCode); | |
| }; | |
| request.onsuccess = (event) => { | |
| db = event.target.result; | |
| log("IndexedDB connected"); | |
| resolve(db); | |
| }; | |
| request.onupgradeneeded = (event) => { | |
| const db = event.target.result; | |
| if (!db.objectStoreNames.contains(STORE_NAME)) { | |
| db.createObjectStore(STORE_NAME, { | |
| keyPath: "id", | |
| autoIncrement: true, | |
| }); | |
| log("Created object store: " + STORE_NAME); | |
| } | |
| }; | |
| }); | |
| } | |
| function saveToIndexedDB(data) { | |
| return new Promise((resolve, reject) => { | |
| if (!db) { | |
| reject("Database not initialized"); | |
| return; | |
| } | |
| const transaction = db.transaction([STORE_NAME], "readwrite"); | |
| const store = transaction.objectStore(STORE_NAME); | |
| const request = store.add({ | |
| data: data, | |
| timestamp: Date.now(), | |
| status: "pending", | |
| }); | |
| request.onsuccess = () => { | |
| log("Data saved to IndexedDB for offline sync"); | |
| resolve(request.result); | |
| }; | |
| request.onerror = (event) => { | |
| log("Error saving to IndexedDB: " + event.target.error); | |
| reject(event.target.error); | |
| }; | |
| }); | |
| } | |
| function getPendingSyncItems() { | |
| return new Promise((resolve, reject) => { | |
| if (!db) { | |
| reject("Database not initialized"); | |
| return; | |
| } | |
| const transaction = db.transaction([STORE_NAME], "readonly"); | |
| const store = transaction.objectStore(STORE_NAME); | |
| const request = store.getAll(); | |
| request.onsuccess = () => { | |
| resolve(request.result); | |
| }; | |
| request.onerror = (event) => { | |
| reject(event.target.error); | |
| }; | |
| }); | |
| } | |
| function removeSyncItem(id) { | |
| return new Promise((resolve, reject) => { | |
| if (!db) { | |
| reject("Database not initialized"); | |
| return; | |
| } | |
| const transaction = db.transaction([STORE_NAME], "readwrite"); | |
| const store = transaction.objectStore(STORE_NAME); | |
| const request = store.delete(id); | |
| request.onsuccess = () => { | |
| resolve(); | |
| }; | |
| request.onerror = (event) => { | |
| reject(event.target.error); | |
| }; | |
| }); | |
| } | |
| // --- Network Status Monitoring --- | |
| let isOnline = navigator.onLine; | |
| function updateNetworkStatus() { | |
| const indicator = document.getElementById("networkIndicator"); | |
| const statusText = document.getElementById("networkStatus"); | |
| if (navigator.onLine) { | |
| indicator.className = "online-indicator online"; | |
| statusText.textContent = "Online"; | |
| isOnline = true; | |
| // Try to sync pending items when we come back online | |
| if (db) { | |
| syncPendingItems(); | |
| } | |
| } else { | |
| indicator.className = "online-indicator offline"; | |
| statusText.textContent = "Offline"; | |
| isOnline = false; | |
| } | |
| } | |
| async function syncPendingItems() { | |
| if (!isOnline) return; | |
| try { | |
| const pendingItems = await getPendingSyncItems(); | |
| if (pendingItems.length > 0) { | |
| log(`Found ${pendingItems.length} items to sync from IndexedDB`); | |
| for (const item of pendingItems) { | |
| // Use the sync subject to process the item | |
| syncSubject.setState(item.data); | |
| // After successful sync, remove from IndexedDB | |
| await removeSyncItem(item.id); | |
| } | |
| log("Completed syncing all pending items from IndexedDB"); | |
| } | |
| } catch (error) { | |
| log("Error syncing pending items: " + error.message); | |
| } | |
| } | |
| window.addEventListener("online", updateNetworkStatus); | |
| window.addEventListener("offline", updateNetworkStatus); | |
| // --- Sync Engine Code --- | |
| // Define a queue for resilient syncing | |
| class SyncQueue { | |
| constructor() { | |
| this.queue = []; | |
| } | |
| enqueue(item) { | |
| this.queue.push({ data: item, retries: 0 }); | |
| } | |
| dequeue() { | |
| return this.queue.shift(); | |
| } | |
| isEmpty() { | |
| return this.queue.length === 0; | |
| } | |
| peek() { | |
| return this.queue[0]; | |
| } | |
| } | |
| // Observer base class | |
| class SyncObserver { | |
| update(data) { | |
| console.log("SyncObserver received:", JSON.stringify(data)); | |
| } | |
| } | |
| // Subject class to manage observers and notify them of changes | |
| class SyncSubject { | |
| constructor() { | |
| this.observers = []; | |
| this.state = { data: null }; | |
| } | |
| addObserver(observer) { | |
| this.observers.push(observer); | |
| } | |
| notify(data) { | |
| this.observers.forEach((observer) => observer.update(data)); | |
| } | |
| setState(newState) { | |
| // Merge the new state into the current state | |
| this.state = Object.assign({}, this.state, newState); | |
| this.notify(this.state); | |
| } | |
| } | |
| // DatabaseSync observer with queue, retry logic, and exponential backoff | |
| class DatabaseSync extends SyncObserver { | |
| constructor(syncServerUrl, maxRetries, maxBackoffTime) { | |
| super(); | |
| this.syncServerUrl = syncServerUrl; | |
| this.queue = new SyncQueue(); | |
| this.isSyncing = false; | |
| this.maxRetries = maxRetries !== undefined ? maxRetries : 5; | |
| this.maxBackoffTime = maxBackoffTime || 30000; // Default max backoff: 30 seconds | |
| this.currentNetworkFailures = 0; | |
| } | |
| update(data) { | |
| this.queue.enqueue(data); | |
| if (!this.isSyncing) this.processQueue(); | |
| } | |
| async processQueue() { | |
| if (!isOnline) { | |
| log("Device is offline, saving data for later sync"); | |
| while (!this.queue.isEmpty()) { | |
| const item = this.queue.dequeue(); | |
| if (item && item.data) { | |
| await saveToIndexedDB(item.data); | |
| } | |
| } | |
| this.isSyncing = false; | |
| return; | |
| } | |
| this.isSyncing = true; | |
| updateSyncStatus("Syncing data..."); | |
| while (!this.queue.isEmpty()) { | |
| const item = this.queue.peek(); | |
| if (!item) break; | |
| const { data, retries } = item; | |
| try { | |
| // Added timeout to the fetch for better error handling | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 5000); | |
| const response = await fetch(this.syncServerUrl, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(data), | |
| signal: controller.signal, | |
| }); | |
| clearTimeout(timeoutId); | |
| if (response.ok) { | |
| log("Data synced successfully: " + JSON.stringify(data)); | |
| this.queue.dequeue(); | |
| this.currentNetworkFailures = 0; // Reset network failure counter | |
| } else { | |
| log(`Sync failed with status: ${response.status}`); | |
| await this.handleRetry(item, response.status >= 500); | |
| } | |
| } catch (error) { | |
| if (error.name === "AbortError") { | |
| log("Sync request timed out"); | |
| } else { | |
| log("Network error: " + error.message); | |
| } | |
| this.currentNetworkFailures++; | |
| await this.handleRetry(item, true); | |
| } | |
| } | |
| this.isSyncing = false; | |
| updateSyncStatus("Sync complete"); | |
| // Check for more items after a delay | |
| if (!this.queue.isEmpty()) { | |
| setTimeout(() => this.processQueue(), 1000); | |
| } | |
| } | |
| async handleRetry(item, isServerError) { | |
| item.retries++; | |
| // Different strategies for different types of errors | |
| if (item.retries > this.maxRetries) { | |
| log( | |
| "Max retries reached for data: " + | |
| JSON.stringify(item.data) + | |
| " - Saving to IndexedDB for later" | |
| ); | |
| // Save to IndexedDB for later retry and remove from current queue | |
| await saveToIndexedDB(item.data); | |
| this.queue.dequeue(); | |
| return; | |
| } | |
| // Calculate backoff time - more aggressive for server errors, | |
| // more conservative for network failures | |
| let delay; | |
| if (isServerError) { | |
| // Exponential backoff for server errors | |
| delay = Math.min( | |
| Math.pow(2, item.retries) * 1000, | |
| this.maxBackoffTime | |
| ); | |
| } else { | |
| // Linear backoff + jitter for other errors | |
| delay = Math.min( | |
| item.retries * 2000 + Math.random() * 1000, | |
| this.maxBackoffTime | |
| ); | |
| } | |
| // If we've had multiple network failures in a row, be more conservative | |
| if (this.currentNetworkFailures > 2) { | |
| delay = Math.min(delay * 2, this.maxBackoffTime); | |
| } | |
| log( | |
| `Retrying in ${(delay / 1000).toFixed(1)} seconds (attempt ${ | |
| item.retries | |
| } of ${this.maxRetries})` | |
| ); | |
| await new Promise((resolve) => setTimeout(resolve, delay)); | |
| } | |
| } | |
| // --- UI Integration --- | |
| // Helper function to log messages in the UI | |
| function log(message) { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| const logDiv = document.getElementById("log"); | |
| const p = document.createElement("p"); | |
| p.textContent = `[${timestamp}] ${message}`; | |
| logDiv.appendChild(p); | |
| // Auto-scroll to bottom | |
| logDiv.scrollTop = logDiv.scrollHeight; | |
| } | |
| function updateSyncStatus(message) { | |
| const statusElement = document.getElementById("syncStatus"); | |
| statusElement.textContent = message; | |
| } | |
| // Custom UI observer to show updates on the page | |
| class UILogObserver extends SyncObserver { | |
| update(data) { | |
| log("UI Observer: Data updated to " + JSON.stringify(data)); | |
| } | |
| } | |
| // Create a sync subject | |
| const syncSubject = new SyncSubject(); | |
| // Create observers | |
| const remoteSyncObserver = new DatabaseSync( | |
| "http://127.0.0.1:5000/data", | |
| 5, | |
| 60000 | |
| ); | |
| const uiLogObserver = new UILogObserver(); | |
| // Attach observers | |
| syncSubject.addObserver(remoteSyncObserver); | |
| syncSubject.addObserver(uiLogObserver); | |
| // CRITICAL FIX: Handle form submission | |
| function handleSyncForm(e) { | |
| // Make sure to prevent the default action | |
| e.preventDefault(); | |
| console.log("Form submission handled and prevented!"); | |
| const input = document.getElementById("dataInput"); | |
| const value = input.value.trim(); | |
| if (!value) return; | |
| // Trigger a state change | |
| syncSubject.setState({ data: value }); | |
| input.value = ""; | |
| return false; | |
| } | |
| // Initialize everything when page is loaded | |
| document.addEventListener("DOMContentLoaded", async function () { | |
| log("DOM loaded, initializing sync engine"); | |
| // CRITICAL FIX: Immediately attach event listener to the form | |
| const syncForm = document.getElementById("syncForm"); | |
| if (syncForm) { | |
| // CRITICAL FIX: Use addEventListener directly on the form | |
| syncForm.addEventListener("submit", handleSyncForm); | |
| log("Form handler attached"); | |
| } else { | |
| console.error("Could not find form with ID 'syncForm'"); | |
| } | |
| // Initialize IndexedDB | |
| try { | |
| await openDatabase(); | |
| // Check for pending sync items | |
| const pendingItems = await getPendingSyncItems(); | |
| if (pendingItems.length > 0) { | |
| log(`Found ${pendingItems.length} offline items ready to sync`); | |
| } | |
| // Check network status and update UI | |
| updateNetworkStatus(); | |
| // Sync any pending items | |
| syncPendingItems(); | |
| updateSyncStatus("Sync engine ready"); | |
| } catch (error) { | |
| log("Initialization error: " + error); | |
| updateSyncStatus("Sync engine error: " + error.message); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment