mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
5.2 KiB
Dart
111 lines
5.2 KiB
Dart
/// Connectivity-gated background push: the offline backstop for the
|
|
/// immediate-push-on-log behaviour. When a meal is logged while the device is
|
|
/// offline the in-process auto-sync fails silently; a WorkManager one-off task
|
|
/// with a `NetworkType.connected` constraint (enqueued on every log) fires on
|
|
/// reconnect and uploads the log without the app being reopened.
|
|
library;
|
|
|
|
import 'dart:developer';
|
|
import 'package:diet_guard_app/services/app_settings_service.dart';
|
|
import 'package:diet_guard_app/services/body_service.dart';
|
|
import 'package:diet_guard_app/services/budget_history_service.dart';
|
|
import 'package:diet_guard_app/services/firebase_client.dart';
|
|
import 'package:diet_guard_app/services/foodbank_service.dart';
|
|
import 'package:diet_guard_app/services/github_client_factory.dart';
|
|
import 'package:diet_guard_app/services/kuchnia_credential_service.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
|
import 'package:diet_guard_app/services/sync_device_id.dart';
|
|
import 'package:diet_guard_app/services/sync_health.dart';
|
|
import 'package:diet_guard_app/services/sync_service.dart';
|
|
import 'package:diet_guard_app/services/sync_settings.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
/// Unique WorkManager task name for the connectivity-gated push.
|
|
const String syncPushTaskName = 'diet_guard.sync_push';
|
|
|
|
/// Loads sync settings and pushes the local log, returning WorkManager's
|
|
/// success flag: `true` when there was nothing to push or the push
|
|
/// succeeded, `false` to ask WorkManager to retry (with backoff) after a
|
|
/// transient failure -- an offline moment or a GitHub hiccup.
|
|
///
|
|
/// Extracted from the WorkManager dispatcher so it is unit-testable without
|
|
/// the real plugin (which only runs as a background isolate on-device),
|
|
/// exactly like [checkAndNotify]. [httpClient] is injectable for the same
|
|
/// reason. The service singletons are (re)initialised here because a fresh
|
|
/// background isolate has none; [LogStorageService.init] is idempotent, so
|
|
/// this is a no-op when a test has already pointed it at a temp dir.
|
|
/// [AppSettingsService.init] must run too: [runSync] now also syncs the
|
|
/// budget, and applies a merge winner via `AppSettingsService.instance`,
|
|
/// which throws if the singleton was never initialised in this isolate.
|
|
Future<bool> backgroundSyncPush({http.Client? httpClient}) async {
|
|
// Must run here too, and before anything stamps an HLC: a fresh isolate has
|
|
// its own static state, so without this `currentSyncDeviceId` falls back to
|
|
// the compile-time role constant ('phone') and this tick publishes to
|
|
// `devices/phone/` instead of the persisted uuid. That splits the device
|
|
// across two directories, and because `sync_state` holds a single
|
|
// `pushedRev` with no notion of which identity wrote it, the next
|
|
// *foreground* push then sees `unchanged` and skips writing the uuid
|
|
// directory entirely.
|
|
await initSyncDeviceId();
|
|
await LogStorageService.init();
|
|
await FoodBankService.init();
|
|
await AppSettingsService.init();
|
|
// runSync also merges the budget history through this singleton.
|
|
await BudgetHistoryService.init();
|
|
// A fresh isolate has its own static state, so the schedule must be loaded
|
|
// here too -- otherwise the due-slot check in this isolate would fall back
|
|
// to the default four meals and nag for checkpoints the user's actual
|
|
// schedule does not have.
|
|
await MealScheduleService.init();
|
|
// Same fresh-isolate reasoning: `_syncKuchniaCredential` no-ops when this
|
|
// singleton is uninitialised, so without this a background tick would
|
|
// neither publish this device's catering credential nor receive a peer's --
|
|
// and on the phone the background tick is most of the syncing there is.
|
|
await KuchniaCredentialService.init();
|
|
// `syncBody` no-ops without it, and the background tick is when the
|
|
// phone ingests the morning weigh-in.
|
|
await BodyService.init();
|
|
final SyncSettings settings;
|
|
try {
|
|
settings = await SyncSettings.load();
|
|
} on Exception catch (error, stackTrace) {
|
|
log(
|
|
'diet_guard background sync: cannot read config in-isolate',
|
|
level: 900,
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
return false; // let WorkManager retry
|
|
}
|
|
// Either backend counts. `isConfigured` means "has a GitHub token", so
|
|
// gating on it alone silently skips every background push on a device
|
|
// connected only to Firebase.
|
|
if (!settings.isConfigured && await openFirebase() == null) {
|
|
log(
|
|
'diet_guard background sync: no backend configured; nothing to push',
|
|
level: 900,
|
|
);
|
|
await SyncHealth.recordUnconfigured();
|
|
return true; // nothing to push; don't retry
|
|
}
|
|
final client = createGitHubClient(settings, httpClient: httpClient);
|
|
try {
|
|
await runSync(await syncBackend(client));
|
|
await SyncHealth.recordSuccess();
|
|
return true;
|
|
} on Exception catch (error, stackTrace) {
|
|
// Never silent: this is the only signal that a background push failed.
|
|
log(
|
|
'diet_guard background sync failed',
|
|
level: 1000,
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
await SyncHealth.recordFailure();
|
|
return false; // offline / transient error -> retry with backoff
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|