// Included by app.cpp; all App mutations happen on its window thread. Bytes App::syncConflictData() { if (loadSyncConfig(data) != conflictConnection) return encodeSyncConflicts({}); return encodeSyncConflicts(pendingSyncConflicts); } std::wstring App::syncCommand(int action, const std::vector &choices) { if (action == 0) return syncStatus; if (syncRunning) return L"Synchronization is already running."; auto c = loadSyncConfig(data); if (!c.enabled) return syncStatus = L"Synchronization is disabled."; validateSyncConfig(c); if (action == 4 && (c != conflictConnection || choices.empty())) throw std::runtime_error("Connection changed or no choices selected. Sync again."); if (busy) return L"Wait for the current text operation to finish."; syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff. if (syncWorker.joinable()) syncWorker.join(); syncConnection = c; syncSnapshot = encodeSettings(settings); auto snapshot = settings; auto availableLayouts = layouts; syncRunning = true; syncDone = false; syncStatus = L"Connecting to OpenBao..."; nextSync = GetTickCount64() + SyncIntervalMs; try { syncWorker = std::thread([this, c, snapshot, availableLayouts, action, choices] { SyncResult result; try { auto provider = makeSyncProvider(c); // An explicit choice also repairs a missing/corrupt local baseline. auto base = action == 1 || action == 4 ? loadSyncBase(data, c) : Bytes{}; result = synchronize(*provider, base, sharedSyncSettings(snapshot), action == 2 ? SyncAction::Download : action == 3 ? SyncAction::Upload : SyncAction::Automatic, [&](const Bytes &b) { restoreSyncSettings(b, snapshot, availableLayouts); }, choices); } catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; } catch (const std::exception &e) { result.status = wide(e.what()); } syncResult = std::move(result); syncDone.store(true); }); } catch (...) { syncRunning = false; throw; } return syncStatus; } void App::pollSync() { try { if (syncRunning && syncDone.load()) { if (busy) return; syncWorker.join(); syncRunning = false; syncDone = false; syncStatus = syncResult.status; if (syncResult.success) { if (loadSyncConfig(data) != syncConnection || encodeSettings(settings) != syncSnapshot) { syncStatus = L"Local settings or connection changed during sync. Checking again shortly."; nextSync = GetTickCount64() + 2000; return; } auto next = restoreSyncSettings(syncResult.settings, settings, layouts); if (encodeSettings(next) != syncSnapshot) { atomicWrite(data / L"settings-before-sync.bin", protect(syncSnapshot)); applySettings(next); } saveSyncBase(data, syncConnection, syncResult.base.empty() ? syncResult.settings : syncResult.base); pendingSyncConflicts = syncResult.conflicts; conflictConnection = syncConnection; SYSTEMTIME time{}; GetLocalTime(&time); wchar_t stamp[32]{}; swprintf(stamp, std::size(stamp), L" (%02u:%02u:%02u)", time.wHour, time.wMinute, time.wSecond); syncStatus += stamp; } // Back off after errors (including bad credentials) instead of repeatedly // hitting userpass lockout. The explicit Sync button remains available. syncRetryAfter = syncResult.success ? 0 : GetTickCount64() + 15 * 60000; nextSync = syncResult.success ? GetTickCount64() + SyncIntervalMs : syncRetryAfter; } if (!syncRunning && GetTickCount64() >= nextSync) { nextSync = GetTickCount64() + SyncIntervalMs; if (loadSyncConfig(data).enabled) syncCommand(1); } } catch (const nlohmann::json::exception &) { syncStatus = L"Invalid local sync configuration or cached profile."; nextSync = GetTickCount64() + SyncIntervalMs; } catch (const std::exception &e) { syncStatus = wide(e.what()); nextSync = GetTickCount64() + SyncIntervalMs; } }