todo-app/lib/sync/run_sync.dart
Krzysztof kuhy Rudnicki f933b4baa4
Some checks are pending
release-apk / release (APK) (push) Waiting to run
Sync through Firebase, mirroring to GitHub, and stop re-downloading peers
Firebase becomes the primary backend when this device has been set up for it;
GitHub stays as a mirror so a device that has not moved yet still converges.
Not being set up is a normal state, not an error -- sync runs over GitHub
exactly as before, which is also the rollback.

This app hand-rolled its own pull-merge-push instead of calling syncLog, so it
had no revision tracking at all: every sync downloaded all four peer note logs
-- roughly 150 KB each -- whether or not anything had changed. It now reads one
small revision map first and skips any peer whose hash is unchanged, and
suppresses its own push when its log is unchanged. That is the single largest
traffic saving in the fleet and what keeps this inside the free tier.

sync() now takes a RemoteStore rather than a GitHubClient, so choosing a
backend is a change at the call site and nothing more.

runSync() is the one place that decides how a sync runs. The capture screen
syncs on a timer and on demand and the settings screen on a button; each used
to build its own client, so a backend change meant editing four call sites and
hoping none was missed.

Credentials are split by what is safe to publish, because this repo is public:
the Web API key and database URL ship in source (both already ship inside the
APK, and the security rules protect the data), while the account email and
password are entered per device and live in the OS keystore.

247 tests at 100% line coverage. Two assertions changed from `contains('PUT')`
to `isNotEmpty`: those tests sync twice, and the second push is now correctly
suppressed as a no-op -- that suppression is the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
2026-08-04 23:36:24 +02:00

63 lines
2.2 KiB
Dart

/// One place that decides *how* a sync runs, for every screen that
/// triggers one.
///
/// The capture screen syncs on a timer and on demand, the settings screen on a
/// button; before this they each built their own client, so a change to the
/// backend meant editing four call sites and hoping none was missed.
library;
import 'package:crdt_sync/crdt_sync.dart';
import 'package:http/http.dart' as http;
import 'package:todo/data/note_repository.dart';
import 'package:todo/sync/firebase_backend.dart';
import 'package:todo/sync/sync_service.dart';
import 'package:todo/sync/sync_settings.dart';
import 'package:todo/sync/sync_state_factory.dart';
/// Runs one sync, choosing the backend and carrying the revision cache.
///
/// Firebase is the primary when this device has been set up for it, with
/// GitHub kept as a mirror so a device that has not moved yet still
/// converges. Not being set up is a normal state, not an error — sync runs
/// over GitHub exactly as it did before.
///
/// [firebaseFactory] and [stateStore] exist for tests, which must not reach
/// the OS keystore or an application-support directory.
Future<SyncResult> runSync(
NoteRepository repository,
SyncSettings settings, {
http.Client? httpClient,
Future<FirebaseRestClient?> Function()? firebaseFactory,
SyncStateStore? stateStore,
}) async {
final github = GitHubClient(
owner: settings.owner,
repo: settings.repo,
token: settings.token,
httpClient: httpClient,
);
final factory = firebaseFactory ?? openFirebase;
final firebase = await factory();
try {
final service = SyncService(
// The production default resolves a real application-support directory
// through path_provider, which has no binding under `flutter test`;
// every test injects a store instead.
// coverage:ignore-start
stateStore: stateStore ?? await openSyncStateStore(),
// coverage:ignore-end
);
return await service.sync(
repository,
firebase == null
? github
: MirrorStore(primary: firebase, mirror: github),
);
} finally {
// Close both: unlike the long-lived desktop callers, each screen builds
// these per run.
github.close();
firebase?.close();
}
}