iOS: device sync (Swift port of the Android engine) behind the sync_service channel

PROPFIND walk / diff / GET / PUT mirror with conflicts, per-account state,
BGTaskScheduler background runs, and conflict notifications with Keep local /
Use server. Reviewed by Codex against the Kotlin original; its data-loss
findings are fixed and each has a regression test (fake-server end-to-end
tests, 68 Swift tests total): truncated/unusable PROPFIND answers are never a
manifest, unreadable state aborts the run, server paths can't leave the mirror,
case/Unicode collisions are skipped, atomic downloads, a locally edited file
deleted on the server is kept, '/' scope matching, serialised removal,
tracked/cancellable background runs, per-account conflicts, and sign-out vs
background-off credentials (cancel gains a 'forget' flag).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-06 21:01:08 -04:00
co-authored by Claude Sonnet 5.5
parent 1634d34341
commit 8805bafb97
14 changed files with 2834 additions and 10 deletions
+19 -5
View File
@@ -105,12 +105,17 @@ class SyncService {
)
: _SyncConfig.fromPrefs(prefs, store.accountPrefKey, account.id);
final password = await store.readPassword(account.id);
if (password == null ||
config.paths.isEmpty ||
config.intervalMinutes == null) {
if (password == null || config.paths.isEmpty) {
await cancelAccount(account.id);
continue;
}
if (config.intervalMinutes == null) {
// Background sync is off, but this account still has paths to sync
// by hand: keep its credentials (native uses them for "Sync now"
// and conflict notification actions).
await cancelAccount(account.id, forget: false);
continue;
}
await invokeIfAvailable(_channel, 'reschedule', {
'accountId': account.id,
'serverUrl': account.serverUrl,
@@ -132,8 +137,17 @@ class SyncService {
/// Stops [accountId]'s periodic job (the account was removed, or has
/// nothing left to sync).
static Future<void> cancelAccount(String accountId) async {
await invokeIfAvailable(_channel, 'cancel', {'accountId': accountId});
///
/// [forget] (the default) is for an account that was signed out or removed:
/// native also drops its stored credentials. Pass false when only the
/// background job is being turned off - the account's paths are still
/// synced by hand ("Sync now"), and a conflict notification's actions
/// still need its credentials.
static Future<void> cancelAccount(String accountId, {bool forget = true}) async {
await invokeIfAvailable(_channel, 'cancel', {
'accountId': accountId,
'forget': forget,
});
}
/// Runs a one-off sync pass immediately, independent of the periodic