mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-15 08:00:47 +01:00
crdt_sync v0.3.0 now provides canAccessRepo() + deleteFile(), so todo syncs through the one shared client like the other apps instead of its own copy. SyncService loses its SHA-tracking (crdt_sync's putFileText self-resolves the sha); lib/sync/github_client.dart + its test are deleted; capture/settings/ sync_smoke repoint to the shared client. flutter analyze clean, 169 tests at 100% coverage, device-verified (27-note backlog intact, no crash). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
79 lines
2.8 KiB
Dart
79 lines
2.8 KiB
Dart
import 'package:crdt_sync/crdt_sync.dart';
|
|
|
|
import '../data/note_repository.dart';
|
|
|
|
/// Outcome of a sync run, for surfacing in the UI.
|
|
class SyncResult {
|
|
const SyncResult({required this.mergedDevices, required this.pushed});
|
|
|
|
/// How many other devices' logs were pulled and merged.
|
|
final int mergedDevices;
|
|
|
|
/// Whether this device pushed its own log.
|
|
final bool pushed;
|
|
|
|
@override
|
|
String toString() =>
|
|
'SyncResult(mergedDevices: $mergedDevices, pushed: $pushed)';
|
|
}
|
|
|
|
/// Synchronises a [NoteRepository] with a GitHub repo used as dumb storage.
|
|
///
|
|
/// Design: each device owns exactly one file, `todo-sync/notes/<nodeId>.json`,
|
|
/// holding its full crdt_sync note [Log]. Because no two devices ever write the
|
|
/// same file, there are **no git-level merge conflicts**; convergence is the
|
|
/// CRDT layer's job — pulling every other device's log and [NoteRepository]
|
|
/// merging it via `mergeLogs` is commutative and idempotent, so repeated syncs
|
|
/// in any order converge to the same state.
|
|
///
|
|
/// The directory is `notes/` (not the legacy `changesets/`): the on-the-wire
|
|
/// format changed from a `sqlite_crdt` changeset to a crdt_sync `Log`, so the
|
|
/// two never share a path and an old client can't misread a new file.
|
|
class SyncService {
|
|
const SyncService({this.notesDir = 'todo-sync/notes'});
|
|
|
|
/// Directory in the repo under which per-device note-log files live.
|
|
final String notesDir;
|
|
|
|
/// Runs a full pull-merge-push cycle. Safe to call repeatedly.
|
|
Future<SyncResult> sync(NoteRepository repo, GitHubClient github) async {
|
|
final nodeId = repo.nodeId;
|
|
final ownFileName = '$nodeId.json';
|
|
|
|
// 1. Pull: list all device log files, merge everyone else's.
|
|
final names = await github.listDirectory(notesDir);
|
|
var merged = 0;
|
|
for (final name in names) {
|
|
if (name == ownFileName) continue; // our own file; skip it.
|
|
final text = await github.getFileText('$notesDir/$name');
|
|
if (text == null) continue;
|
|
final remote = _decodeLog(text);
|
|
if (remote == null) continue; // Skip a corrupt/foreign file.
|
|
await repo.importLog(remote);
|
|
merged++;
|
|
}
|
|
|
|
// 2. Push: upload our own (now-merged) log under our node id.
|
|
// putFileText resolves our file's current sha itself.
|
|
await github.putFileText(
|
|
'$notesDir/$ownFileName',
|
|
logToJson(repo.exportLog()),
|
|
message: 'sync: $nodeId @ ${DateTime.now().toUtc().toIso8601String()}',
|
|
);
|
|
|
|
return SyncResult(mergedDevices: merged, pushed: true);
|
|
}
|
|
|
|
/// Parses a remote note log, returning `null` for a corrupt or wrong-shape
|
|
/// payload so one bad device file never aborts the whole sync.
|
|
Log? _decodeLog(String text) {
|
|
try {
|
|
return logFromJson(text);
|
|
} on FormatException {
|
|
return null;
|
|
} on TypeError {
|
|
return null;
|
|
}
|
|
}
|
|
}
|