Skip to content

Instantly share code, notes, and snippets.

@MikeTeddyOmondi
Last active April 26, 2025 14:10
Show Gist options
  • Select an option

  • Save MikeTeddyOmondi/f8d358a2cd5cd87ee3d806af77612be9 to your computer and use it in GitHub Desktop.

Select an option

Save MikeTeddyOmondi/f8d358a2cd5cd87ee3d806af77612be9 to your computer and use it in GitHub Desktop.
Sync engine - JavaScript
<!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