feat: add connectivity check for multiplayer training and retry logic for sync
This commit is contained in:
parent
1e573904f2
commit
2e6052e69d
3 changed files with 267 additions and 200 deletions
12
lib/src/core/utils/connectivity_helper.dart
Normal file
12
lib/src/core/utils/connectivity_helper.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class ConnectivityHelper {
|
||||||
|
static Future<bool> hasConnection() async {
|
||||||
|
try {
|
||||||
|
final result = await InternetAddress.lookup('8.8.8.8');
|
||||||
|
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
|
||||||
|
} on SocketException catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import '../widgets/level_display.dart';
|
||||||
import '../widgets/start_raid_button.dart';
|
import '../widgets/start_raid_button.dart';
|
||||||
import '../../../gamification/application/quest_service.dart';
|
import '../../../gamification/application/quest_service.dart';
|
||||||
import '../../../workout_runner/application/workout_generator_service.dart';
|
import '../../../workout_runner/application/workout_generator_service.dart';
|
||||||
|
import '../../../../core/utils/connectivity_helper.dart';
|
||||||
|
|
||||||
class HubScreen extends ConsumerStatefulWidget {
|
class HubScreen extends ConsumerStatefulWidget {
|
||||||
const HubScreen({super.key});
|
const HubScreen({super.key});
|
||||||
|
|
@ -51,6 +52,26 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleMultiplayerPress() async {
|
||||||
|
final hasInternet = await ConnectivityHelper.hasConnection();
|
||||||
|
|
||||||
|
if (!hasInternet) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Für Multiplayer wird eine aktive Internetverbindung benötigt.'), //TODO: make this in l10n
|
||||||
|
backgroundColor: AppTheme.errorColor,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) _showMultiplayerDialog();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _startNextWorkout(
|
Future<void> _startNextWorkout(
|
||||||
CycleCollection cycle, UserCollection user) async {
|
CycleCollection cycle, UserCollection user) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -422,7 +443,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
icon: const Icon(Icons.groups,
|
icon: const Icon(Icons.groups,
|
||||||
color: AppTheme.secondaryColor),
|
color: AppTheme.secondaryColor),
|
||||||
tooltip: 'Multiplayer Lobby',
|
tooltip: 'Multiplayer Lobby',
|
||||||
onPressed: _showMultiplayerDialog,
|
onPressed: _handleMultiplayerPress,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import '../../../core/constants/app_constants.dart';
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../core/utils/connectivity_helper.dart';
|
||||||
import '../local/app_database.dart';
|
import '../local/app_database.dart';
|
||||||
import '../repositories/user_repository.dart';
|
import '../repositories/user_repository.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
|
@ -25,211 +29,241 @@ class SyncService {
|
||||||
|
|
||||||
Future<void> sync() async {
|
Future<void> sync() async {
|
||||||
if (_isSyncing) return;
|
if (_isSyncing) return;
|
||||||
|
|
||||||
|
if (!await ConnectivityHelper.hasConnection()) {
|
||||||
|
log('Sync skipped: No internet connection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log('🔄 Starting Sync...');
|
await _retry(_performSync, maxAttempts: 3);
|
||||||
|
|
||||||
final dirtyCycles = await (db.select(db.cycles)
|
|
||||||
..where((c) => c.isDirty.equals(true)))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (var cycle in dirtyCycles) {
|
|
||||||
try {
|
|
||||||
if (cycle.serverId == null) {
|
|
||||||
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
|
||||||
final tmsMap = cycle.trainingMaxes
|
|
||||||
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
|
||||||
|
|
||||||
final response = await apiClient.createCycle(tmsMap);
|
|
||||||
final newServerId = response['id'];
|
|
||||||
|
|
||||||
await db.transaction(() async {
|
|
||||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
|
||||||
.write(
|
|
||||||
CyclesCompanion(
|
|
||||||
serverId: Value(newServerId),
|
|
||||||
isDirty: const Value(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final oldLocalIdRef = cycle.id.toString();
|
|
||||||
await (db.update(db.workouts)
|
|
||||||
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
|
||||||
.write(
|
|
||||||
WorkoutsCompanion(
|
|
||||||
cycleId: Value(newServerId),
|
|
||||||
isDirty: const Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
|
||||||
.write(const CyclesCompanion(isDirty: Value(false)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log('❌ Failed to sync cycle ${cycle.id}: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final dirtyUser = await (db.select(db.users)
|
|
||||||
..where((u) => u.isDirty.equals(true)))
|
|
||||||
.getSingleOrNull();
|
|
||||||
final dirtyWorkouts = await (db.select(db.workouts)
|
|
||||||
..where((w) => w.isDirty.equals(true)))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
final validWorkouts =
|
|
||||||
dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
|
|
||||||
|
|
||||||
final pushData = <String, dynamic>{
|
|
||||||
'workouts': validWorkouts.map((w) {
|
|
||||||
return {
|
|
||||||
'id': w.serverId,
|
|
||||||
'local_id': w.id,
|
|
||||||
'cycle_id': w.cycleId,
|
|
||||||
'week': w.week,
|
|
||||||
'day': w.day,
|
|
||||||
'completed_at': w.completedAt?.toIso8601String(),
|
|
||||||
'xp_earned': w.xpEarned,
|
|
||||||
'notes': w.notes,
|
|
||||||
'exercises': w.exercises,
|
|
||||||
};
|
|
||||||
}).toList(),
|
|
||||||
'user_stats': dirtyUser != null
|
|
||||||
? {
|
|
||||||
'xp': dirtyUser.xp,
|
|
||||||
'level': dirtyUser.level,
|
|
||||||
'current_bodyweight': dirtyUser.currentBodyweight,
|
|
||||||
'exercise_variants': dirtyUser.exerciseVariants,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
|
||||||
|
|
||||||
if ((pushData['workouts'] as List).isNotEmpty ||
|
|
||||||
pushData['user_stats'] != null) {
|
|
||||||
log('📤 Pushing data...');
|
|
||||||
final response = await apiClient.sync(
|
|
||||||
lastSyncTimestamp: lastSync ?? '',
|
|
||||||
pushData: pushData,
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.transaction(() async {
|
|
||||||
if (dirtyUser != null) {
|
|
||||||
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
|
|
||||||
.write(const UsersCompanion(isDirty: Value(false)));
|
|
||||||
}
|
|
||||||
for (var w in validWorkouts) {
|
|
||||||
await (db.update(db.workouts)..where((dw) => dw.id.equals(w.id)))
|
|
||||||
.write(const WorkoutsCompanion(isDirty: Value(false)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response['pull_data'] != null) {
|
|
||||||
if (response['pull_data']['cycles'] != null) {
|
|
||||||
final pulledCycles = response['pull_data']['cycles'] as List;
|
|
||||||
for (var cJson in pulledCycles) {
|
|
||||||
final serverId = cJson['id'] as String;
|
|
||||||
final existing = await (db.select(db.cycles)
|
|
||||||
..where((c) => c.serverId.equals(serverId)))
|
|
||||||
.getSingleOrNull();
|
|
||||||
|
|
||||||
final tms = cJson['training_maxes'] as Map<String, dynamic>;
|
|
||||||
final companion = CyclesCompanion(
|
|
||||||
serverId: Value(serverId),
|
|
||||||
userId: Value(cJson['user_id']),
|
|
||||||
cycleNumber: Value(cJson['cycle_number']),
|
|
||||||
startDate: Value(DateTime.parse(cJson['start_date'])),
|
|
||||||
endDate: Value(DateTime.tryParse(cJson['end_date'] ?? '')),
|
|
||||||
isActive: Value(cJson['is_active'] ?? false),
|
|
||||||
trainingMaxes: Value(tms),
|
|
||||||
isDirty: const Value(false),
|
|
||||||
updatedAt: Value(DateTime.now()),
|
|
||||||
createdAt: existing == null
|
|
||||||
? Value(DateTime.now())
|
|
||||||
: const Value.absent(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
await (db.update(db.cycles)
|
|
||||||
..where((c) => c.id.equals(existing.id)))
|
|
||||||
.write(companion);
|
|
||||||
} else {
|
|
||||||
await db.into(db.cycles).insert(companion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response['pull_data']['workouts'] != null) {
|
|
||||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
|
||||||
log('📥 Pulled ${pulledWorkouts.length} workouts.');
|
|
||||||
|
|
||||||
for (var wJson in pulledWorkouts) {
|
|
||||||
final serverId = wJson['id'] as String;
|
|
||||||
final cycleId = wJson['cycle_id'] as String;
|
|
||||||
final week = wJson['week'] as int;
|
|
||||||
final day = wJson['day'] as int;
|
|
||||||
|
|
||||||
var existing = await (db.select(db.workouts)
|
|
||||||
..where((w) => w.serverId.equals(serverId)))
|
|
||||||
.getSingleOrNull();
|
|
||||||
|
|
||||||
if (existing == null) {
|
|
||||||
final candidates = await (db.select(db.workouts)
|
|
||||||
..where((w) =>
|
|
||||||
w.cycleId.equals(cycleId) &
|
|
||||||
w.week.equals(week) &
|
|
||||||
w.day.equals(day)))
|
|
||||||
.get();
|
|
||||||
if (candidates.isNotEmpty) {
|
|
||||||
existing = candidates.first;
|
|
||||||
log('🔄 Merging local workout ${existing.id} with server ID $serverId');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final companion = WorkoutsCompanion(
|
|
||||||
serverId: Value(serverId),
|
|
||||||
cycleId: Value(cycleId),
|
|
||||||
userId: Value(wJson['user_id']),
|
|
||||||
week: Value(week),
|
|
||||||
day: Value(day),
|
|
||||||
completedAt:
|
|
||||||
Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
|
|
||||||
xpEarned: Value(wJson['xp_earned'] ?? 0),
|
|
||||||
exercises: Value(wJson['exercises'] ?? []),
|
|
||||||
notes: Value(wJson['notes'] ?? ''),
|
|
||||||
isDirty: const Value(false),
|
|
||||||
updatedAt: Value(DateTime.now()),
|
|
||||||
createdAt: existing == null
|
|
||||||
? Value(DateTime.now())
|
|
||||||
: const Value.absent(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
await (db.update(db.workouts)
|
|
||||||
..where((w) => w.id.equals(existing!.id)))
|
|
||||||
.write(companion);
|
|
||||||
} else {
|
|
||||||
await db.into(db.workouts).insert(companion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response['server_timestamp'] != null) {
|
|
||||||
await _storage.write(
|
|
||||||
key: AppConstants.keyLastSync,
|
|
||||||
value: response['server_timestamp']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log('✅ Sync completed successfully');
|
log('✅ Sync completed successfully');
|
||||||
} catch (e, stack) {
|
} catch (e) {
|
||||||
log('❌ Sync failed: $e');
|
log('❌ Sync failed finally: $e');
|
||||||
log(stack.toString());
|
|
||||||
} finally {
|
} finally {
|
||||||
_isSyncing = false;
|
_isSyncing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _performSync() async {
|
||||||
|
log('🔄 Starting Sync...');
|
||||||
|
|
||||||
|
final dirtyCycles = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.isDirty.equals(true)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (var cycle in dirtyCycles) {
|
||||||
|
if (cycle.serverId == null) {
|
||||||
|
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
|
||||||
|
final tmsMap = cycle.trainingMaxes
|
||||||
|
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||||
|
|
||||||
|
final response = await apiClient.createCycle(tmsMap);
|
||||||
|
final newServerId = response['id'];
|
||||||
|
|
||||||
|
await db.transaction(() async {
|
||||||
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
|
.write(
|
||||||
|
CyclesCompanion(
|
||||||
|
serverId: Value(newServerId),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final oldLocalIdRef = cycle.id.toString();
|
||||||
|
await (db.update(db.workouts)
|
||||||
|
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
||||||
|
.write(
|
||||||
|
WorkoutsCompanion(
|
||||||
|
cycleId: Value(newServerId),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
|
.write(const CyclesCompanion(isDirty: Value(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final dirtyUser = await (db.select(db.users)
|
||||||
|
..where((u) => u.isDirty.equals(true)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
final dirtyWorkouts = await (db.select(db.workouts)
|
||||||
|
..where((w) => w.isDirty.equals(true)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final validWorkouts =
|
||||||
|
dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
|
||||||
|
|
||||||
|
final pushData = <String, dynamic>{
|
||||||
|
'workouts': validWorkouts.map((w) {
|
||||||
|
return {
|
||||||
|
'id': w.serverId,
|
||||||
|
'local_id': w.id,
|
||||||
|
'cycle_id': w.cycleId,
|
||||||
|
'week': w.week,
|
||||||
|
'day': w.day,
|
||||||
|
'completed_at': w.completedAt?.toIso8601String(),
|
||||||
|
'xp_earned': w.xpEarned,
|
||||||
|
'notes': w.notes,
|
||||||
|
'exercises': w.exercises,
|
||||||
|
};
|
||||||
|
}).toList(),
|
||||||
|
'user_stats': dirtyUser != null
|
||||||
|
? {
|
||||||
|
'xp': dirtyUser.xp,
|
||||||
|
'level': dirtyUser.level,
|
||||||
|
'current_bodyweight': dirtyUser.currentBodyweight,
|
||||||
|
'exercise_variants': dirtyUser.exerciseVariants,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||||
|
|
||||||
|
// if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { ... }
|
||||||
|
|
||||||
|
log('Checking for updates...');
|
||||||
|
final response = await apiClient.sync(
|
||||||
|
lastSyncTimestamp: lastSync ?? '',
|
||||||
|
pushData: pushData,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.transaction(() async {
|
||||||
|
if (dirtyUser != null) {
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
|
||||||
|
.write(const UsersCompanion(isDirty: Value(false)));
|
||||||
|
}
|
||||||
|
for (var w in validWorkouts) {
|
||||||
|
await (db.update(db.workouts)..where((dw) => dw.id.equals(w.id)))
|
||||||
|
.write(const WorkoutsCompanion(isDirty: Value(false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response['pull_data'] != null) {
|
||||||
|
if (response['pull_data']['cycles'] != null) {
|
||||||
|
final pulledCycles = response['pull_data']['cycles'] as List;
|
||||||
|
for (var cJson in pulledCycles) {
|
||||||
|
final serverId = cJson['id'] as String;
|
||||||
|
final existing = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.serverId.equals(serverId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
final tms = cJson['training_maxes'] as Map<String, dynamic>;
|
||||||
|
final companion = CyclesCompanion(
|
||||||
|
serverId: Value(serverId),
|
||||||
|
userId: Value(cJson['user_id']),
|
||||||
|
cycleNumber: Value(cJson['cycle_number']),
|
||||||
|
startDate: Value(DateTime.parse(cJson['start_date'])),
|
||||||
|
endDate: Value(DateTime.tryParse(cJson['end_date'] ?? '')),
|
||||||
|
isActive: Value(cJson['is_active'] ?? false),
|
||||||
|
trainingMaxes: Value(tms),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
createdAt: existing == null
|
||||||
|
? Value(DateTime.now())
|
||||||
|
: const Value.absent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
await (db.update(db.cycles)
|
||||||
|
..where((c) => c.id.equals(existing.id)))
|
||||||
|
.write(companion);
|
||||||
|
} else {
|
||||||
|
await db.into(db.cycles).insert(companion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response['pull_data']['workouts'] != null) {
|
||||||
|
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||||
|
log('📥 Pulled ${pulledWorkouts.length} workouts.');
|
||||||
|
|
||||||
|
for (var wJson in pulledWorkouts) {
|
||||||
|
final serverId = wJson['id'] as String;
|
||||||
|
final cycleId = wJson['cycle_id'] as String;
|
||||||
|
final week = wJson['week'] as int;
|
||||||
|
final day = wJson['day'] as int;
|
||||||
|
|
||||||
|
var existing = await (db.select(db.workouts)
|
||||||
|
..where((w) => w.serverId.equals(serverId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
final candidates = await (db.select(db.workouts)
|
||||||
|
..where((w) =>
|
||||||
|
w.cycleId.equals(cycleId) &
|
||||||
|
w.week.equals(week) &
|
||||||
|
w.day.equals(day)))
|
||||||
|
.get();
|
||||||
|
if (candidates.isNotEmpty) {
|
||||||
|
existing = candidates.first;
|
||||||
|
log('🔄 Merging local workout ${existing.id} with server ID $serverId');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final companion = WorkoutsCompanion(
|
||||||
|
serverId: Value(serverId),
|
||||||
|
cycleId: Value(cycleId),
|
||||||
|
userId: Value(wJson['user_id']),
|
||||||
|
week: Value(week),
|
||||||
|
day: Value(day),
|
||||||
|
completedAt:
|
||||||
|
Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
|
||||||
|
xpEarned: Value(wJson['xp_earned'] ?? 0),
|
||||||
|
exercises: Value(wJson['exercises'] ?? []),
|
||||||
|
notes: Value(wJson['notes'] ?? ''),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
createdAt: existing == null
|
||||||
|
? Value(DateTime.now())
|
||||||
|
: const Value.absent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
await (db.update(db.workouts)
|
||||||
|
..where((w) => w.id.equals(existing!.id)))
|
||||||
|
.write(companion);
|
||||||
|
} else {
|
||||||
|
await db.into(db.workouts).insert(companion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response['server_timestamp'] != null) {
|
||||||
|
await _storage.write(
|
||||||
|
key: AppConstants.keyLastSync, value: response['server_timestamp']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _retry<T>(
|
||||||
|
Future<T> Function() function, {
|
||||||
|
int maxAttempts = 3,
|
||||||
|
Duration initialDelay = const Duration(seconds: 2),
|
||||||
|
}) async {
|
||||||
|
int attempts = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
attempts++;
|
||||||
|
return await function();
|
||||||
|
} catch (e) {
|
||||||
|
final shouldRetry = (e is SocketException) ||
|
||||||
|
(e.toString().contains('500')) ||
|
||||||
|
(e.toString().contains('502')) ||
|
||||||
|
(e.toString().contains('timeout'));
|
||||||
|
|
||||||
|
if (attempts >= maxAttempts || !shouldRetry) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
final delay = initialDelay * (1 << (attempts - 1)); // 2s, 4s, 8s
|
||||||
|
log('⚠️ Sync attempt $attempts failed. Retrying in ${delay.inSeconds}s... Error: $e');
|
||||||
|
await Future.delayed(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue