cyberhybridhub/lib/repositories/user_profile_repository.dart
2026-05-20 10:22:58 -05:00

249 lines
7.0 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../data/local/app_database.dart';
import '../data/local/user_profile_local_store.dart';
import '../data/remote/profile_api_exception.dart';
import '../data/remote/user_profile_remote_store.dart';
import '../data/sync/connectivity_service.dart';
import '../data/sync/profile_sync_coordinator.dart';
import '../models/app_user.dart';
import '../models/sync_result.dart';
import '../models/user_profile.dart';
/// Facade for profile reads/writes with Drift (native) and API sync.
class UserProfileRepository {
UserProfileRepository._();
static final UserProfileRepository instance = UserProfileRepository._();
final ConnectivityService _connectivity = ConnectivityService();
final UserProfileRemoteStore _remote = UserProfileRemoteStore();
AppDatabase? _database;
UserProfileLocalStore? _local;
ProfileSyncCoordinator? _sync;
StreamSubscription<bool>? _connectivitySubscription;
final StreamController<UserProfile?> _profileController =
StreamController<UserProfile?>.broadcast();
final StreamController<ProfileSyncStatus> _syncStatusController =
StreamController<ProfileSyncStatus>.broadcast();
StreamSubscription<UserProfile?>? _localWatchSubscription;
String? _activeUid;
UserProfile? _cachedProfile;
Stream<UserProfile?> get profileStream => _profileController.stream;
Stream<ProfileSyncStatus> get syncStatusStream =>
_syncStatusController.stream;
UserProfile? get currentProfile => _cachedProfile;
bool get usesLocalStore => _local != null;
Future<void> initialize() async {
await _connectivity.initialize();
if (!kIsWeb) {
_database = AppDatabase();
_local = UserProfileLocalStore(_database!);
_sync = ProfileSyncCoordinator(
local: _local,
remote: _remote,
connectivity: _connectivity,
);
} else {
_sync = ProfileSyncCoordinator(
local: null,
remote: _remote,
connectivity: _connectivity,
);
}
_connectivitySubscription = _connectivity.onlineChanges.listen((
bool online,
) {
if (online && _activeUid != null) {
unawaited(sync());
} else if (!online) {
_emitSyncStatus(ProfileSyncStatus.offline);
}
});
_emitSyncStatus(ProfileSyncStatus.idle);
}
Future<void> startSession(AppUser user) async {
if (_activeUid == user.uid) {
return;
}
await endSession();
_activeUid = user.uid;
_emitSyncStatus(ProfileSyncStatus.syncing);
if (_local != null) {
_localWatchSubscription = _local!.watchProfile(user.uid).listen(
(UserProfile? profile) {
_cachedProfile = profile;
_profileController.add(profile);
},
);
UserProfile? profile = await _local!.getProfile(user.uid);
profile ??= UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
await _local!.saveProfile(profile, dirty: true);
_cachedProfile = profile;
_profileController.add(profile);
} else {
await _loadRemoteProfile(user);
}
await sync();
}
Future<void> _loadRemoteProfile(AppUser user) async {
try {
UserProfile? profile = await _remote.fetchProfile();
profile ??= UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
if (profile.revision == 1 && !profile.dirty) {
final UserProfile seeded = profile.copyWith(dirty: true);
profile = await _remote.pushProfile(seeded);
}
_cachedProfile = profile;
_profileController.add(profile);
} catch (_) {
final UserProfile fallback = UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
_cachedProfile = fallback;
_profileController.add(fallback);
_emitSyncStatus(ProfileSyncStatus.error);
}
}
Future<void> endSession() async {
await _localWatchSubscription?.cancel();
_localWatchSubscription = null;
if (_activeUid != null && _local != null) {
await _local!.clearLocal(_activeUid!);
}
_activeUid = null;
_cachedProfile = null;
_profileController.add(null);
_emitSyncStatus(ProfileSyncStatus.idle);
}
Future<void> updateProfile(UserProfile profile) async {
final UserProfile updated = profile.copyWith(
revision: profile.revision + 1,
updatedAt: DateTime.now().toUtc(),
dirty: true,
);
if (_local != null) {
await _local!.saveProfile(updated, dirty: true);
} else {
_cachedProfile = updated;
_profileController.add(updated);
await _remote.pushProfile(updated);
}
unawaited(sync());
}
Future<SyncResult> sync() async {
final String? uid = _activeUid;
if (uid == null || _sync == null) {
return const SyncResult.error('No active session');
}
if (!kIsWeb && !_connectivity.isOnline) {
_emitSyncStatus(ProfileSyncStatus.offline);
return const SyncResult.offline();
}
_emitSyncStatus(ProfileSyncStatus.syncing);
final SyncResult result = _local == null
? await _syncWebOnly()
: await _sync!.sync(uid);
switch (result.kind) {
case SyncResultKind.success:
case SyncResultKind.conflictResolved:
_emitSyncStatus(ProfileSyncStatus.synced);
case SyncResultKind.offline:
_emitSyncStatus(ProfileSyncStatus.offline);
case SyncResultKind.error:
_emitSyncStatus(ProfileSyncStatus.error);
}
return result;
}
Future<SyncResult> _syncWebOnly() async {
final UserProfile? profile = _cachedProfile;
if (profile == null) {
return const SyncResult.error('No profile loaded');
}
try {
if (profile.dirty) {
try {
final UserProfile saved = await _remote.pushProfile(profile);
_cachedProfile = saved;
_profileController.add(saved);
} on ProfileConflictException catch (e) {
_cachedProfile = e.serverProfile;
_profileController.add(e.serverProfile);
return const SyncResult.conflictResolved();
}
} else {
final UserProfile? server = await _remote.fetchProfile();
if (server != null) {
_cachedProfile = server;
_profileController.add(server);
}
}
return const SyncResult.success();
} on ProfileApiException catch (e) {
return SyncResult.error(e.toString());
} catch (e) {
return SyncResult.error(e.toString());
}
}
Future<void> dispose() async {
await _connectivitySubscription?.cancel();
_connectivity.dispose();
await _localWatchSubscription?.cancel();
await _profileController.close();
await _syncStatusController.close();
await _database?.close();
}
void _emitSyncStatus(ProfileSyncStatus status) {
if (!_syncStatusController.isClosed) {
_syncStatusController.add(status);
}
}
}