fix(watch): stabilise stats live, foreground et resync apres perte de connexion (#179-#189)
Reduit le cout radio des samples live et la cadence des projections telephone -> montre (#179-#183). Restaure les statistiques live FC/distance/calories et le maintien foreground/ongoing activity (#184-#185). Fiabilise le demarrage de seance et l'orchestration des permissions montre (#187). Renforce la resynchronisation des statistiques live et du score apres perte puis retour de connexion (#188-#189). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -37,7 +37,8 @@ final class WatchSessionUiState {
|
||||
final WatchCommandAckEvent? lastAck;
|
||||
final WatchSensorSample? sensorSample;
|
||||
|
||||
bool get actionsEnabled => !commandPending && !connectionLost;
|
||||
bool get actionsEnabled =>
|
||||
!commandPending && !connectionLost && !staleProjection;
|
||||
|
||||
WatchSessionUiState copyWith({
|
||||
WatchSessionProjection? projection,
|
||||
@ -112,9 +113,6 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
);
|
||||
unawaited(_nativeClient.requestCapabilityRefresh());
|
||||
unawaited(_nativeClient.requestResync());
|
||||
_freshnessTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
_syncFreshnessState();
|
||||
});
|
||||
}
|
||||
|
||||
final NativeWatchBridgeClient _nativeClient;
|
||||
@ -136,6 +134,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
final _pendingScoreCommandIds = <String>{};
|
||||
double? _optimisticManualScoreValue;
|
||||
DateTime? _lastProjectionReceivedAt;
|
||||
bool _requiresAuthoritativeProjection = true;
|
||||
var _commandCounter = 0;
|
||||
var _commandFailureSerial = 0;
|
||||
|
||||
@ -211,6 +210,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
|
||||
Future<void> _sendCommand(WatchCommandType type) async {
|
||||
if (!value.actionsEnabled ||
|
||||
_requiresAuthoritativeProjection ||
|
||||
(_requiresActiveSession(type) &&
|
||||
value.projection.deviceSessionId.isEmpty)) {
|
||||
return;
|
||||
@ -263,6 +263,8 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
Future<void> _sendScoreCommand(WatchCommandType type, int delta) async {
|
||||
final projection = value.projection;
|
||||
if (value.connectionLost ||
|
||||
value.staleProjection ||
|
||||
_requiresAuthoritativeProjection ||
|
||||
!projection.phoneReachable ||
|
||||
!projection.hasManualScore ||
|
||||
projection.deviceSessionId.isEmpty) {
|
||||
@ -313,6 +315,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
void _handleProjection(WatchSessionProjection projection) {
|
||||
final previousProjection = value.projection;
|
||||
_lastProjectionReceivedAt = DateTime.now();
|
||||
_requiresAuthoritativeProjection = false;
|
||||
_scheduleProjectionExpiry(projection);
|
||||
_pendingCommand = null;
|
||||
_clearCommandTimers();
|
||||
@ -334,6 +337,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
: null,
|
||||
);
|
||||
_triggerProjectionHaptic(previousProjection, projection);
|
||||
_scheduleFreshnessCheck();
|
||||
}
|
||||
|
||||
void _handleSensorSample(WatchSensorSample sample) {
|
||||
@ -403,34 +407,30 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
|
||||
void _handleConnectionEvent(WatchBridgeConnectionEvent event) {
|
||||
value = value.copyWith(connectionLost: !event.isReachable);
|
||||
if (!event.isReachable) {
|
||||
_requiresAuthoritativeProjection = true;
|
||||
_clearScorePending(recalibrate: true);
|
||||
return;
|
||||
}
|
||||
if (event.isReachable || event.requestsResync) {
|
||||
_requiresAuthoritativeProjection = true;
|
||||
unawaited(_nativeClient.requestResync());
|
||||
}
|
||||
}
|
||||
|
||||
void _syncFreshnessState() {
|
||||
final receivedAt = _lastProjectionReceivedAt;
|
||||
if (receivedAt == null) {
|
||||
_freshnessTimer = null;
|
||||
if (_lastProjectionReceivedAt == null) {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final expiryAge = Duration(
|
||||
milliseconds: _projectionTtlMs(value.projection),
|
||||
);
|
||||
final expired =
|
||||
value.projection.deviceSessionId.isNotEmpty &&
|
||||
now.difference(receivedAt) >= expiryAge &&
|
||||
_pendingCommand == null &&
|
||||
_pendingScoreCommandIds.isEmpty;
|
||||
if (expired) {
|
||||
_invalidateExpiredProjection();
|
||||
if (!value.staleProjection) {
|
||||
_requiresAuthoritativeProjection = true;
|
||||
value = value.copyWith(staleProjection: true);
|
||||
_scheduleFreshnessCheck();
|
||||
return;
|
||||
}
|
||||
final age = now.difference(receivedAt);
|
||||
final stale = age >= _staleProjectionThreshold;
|
||||
final lost = age >= _connectionLostThreshold;
|
||||
if (stale != value.staleProjection || lost != value.connectionLost) {
|
||||
value = value.copyWith(staleProjection: stale, connectionLost: lost);
|
||||
if (!value.connectionLost) {
|
||||
value = value.copyWith(connectionLost: true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,6 +446,8 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer = null;
|
||||
_lastProjectionReceivedAt = null;
|
||||
_freshnessTimer?.cancel();
|
||||
_freshnessTimer = null;
|
||||
value = WatchSessionUiState(
|
||||
projection: _expiredProjection(),
|
||||
connectionLost: true,
|
||||
@ -474,6 +476,30 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleFreshnessCheck() {
|
||||
_freshnessTimer?.cancel();
|
||||
_freshnessTimer = null;
|
||||
final receivedAt = _lastProjectionReceivedAt;
|
||||
if (receivedAt == null) {
|
||||
return;
|
||||
}
|
||||
final Duration? nextThreshold;
|
||||
if (!value.staleProjection) {
|
||||
nextThreshold = _staleProjectionThreshold;
|
||||
} else if (!value.connectionLost) {
|
||||
nextThreshold = _connectionLostThreshold - _staleProjectionThreshold;
|
||||
} else {
|
||||
nextThreshold = null;
|
||||
}
|
||||
if (nextThreshold == null) {
|
||||
return;
|
||||
}
|
||||
_freshnessTimer = Timer(
|
||||
nextThreshold.isNegative ? Duration.zero : nextThreshold,
|
||||
_syncFreshnessState,
|
||||
);
|
||||
}
|
||||
|
||||
void _clearCommandTimers() {
|
||||
_waitingTimer?.cancel();
|
||||
_waitingTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user