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:
@ -18,36 +18,89 @@ void main() {
|
||||
final source = _FakeProjectionSource(_projection(revision: 0));
|
||||
final adapter = _adapter(native: native, source: source);
|
||||
await adapter.start();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
native.published.clear();
|
||||
|
||||
source.emit(_projection(revision: 1));
|
||||
source.emit(_projection(revision: 2));
|
||||
source.emit(_projection(revision: 3));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(native.published.map((projection) => projection.revision), [1, 2]);
|
||||
expect(native.published.map((projection) => projection.revision), [2, 3]);
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
test('heartbeats while a timer is running', () async {
|
||||
test('republishes volatile heartbeats as non urgent keepalives', () async {
|
||||
final native = _FakeWatchBridgeNativeChannel();
|
||||
final source = _FakeProjectionSource(_runningProjection(revision: 1));
|
||||
final source = _FakeProjectionSource(
|
||||
_runningProjection(revision: 1),
|
||||
incrementsRevisionOnEmit: false,
|
||||
);
|
||||
final adapter = _adapter(
|
||||
native: native,
|
||||
source: source,
|
||||
heartbeatInterval: const Duration(milliseconds: 10),
|
||||
);
|
||||
await adapter.start();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
native.published.clear();
|
||||
native.urgentFlags.clear();
|
||||
source.emitCount = 0;
|
||||
|
||||
await adapter.publish(_runningProjection(revision: 1));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 35));
|
||||
|
||||
expect(source.emitCount, greaterThanOrEqualTo(1));
|
||||
expect(native.published.length, greaterThanOrEqualTo(2));
|
||||
expect(native.published.length, greaterThanOrEqualTo(1));
|
||||
expect(native.urgentFlags, everyElement(false));
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
test('marks only projection revision changes as urgent', () async {
|
||||
final native = _FakeWatchBridgeNativeChannel();
|
||||
final source = _FakeProjectionSource(_runningProjection(revision: 0));
|
||||
final adapter = _adapter(native: native, source: source);
|
||||
await adapter.start();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
native.published.clear();
|
||||
native.urgentFlags.clear();
|
||||
|
||||
await adapter.publish(_runningProjection(revision: 2));
|
||||
await adapter.publish(_runningProjection(revision: 2));
|
||||
|
||||
expect(native.published.map((projection) => projection.revision), [2]);
|
||||
expect(native.urgentFlags, [true]);
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
test(
|
||||
'forces an urgent projection resync even when state is unchanged',
|
||||
() async {
|
||||
final native = _FakeWatchBridgeNativeChannel();
|
||||
final source = _FakeProjectionSource(
|
||||
_runningProjection(revision: 1),
|
||||
incrementsRevisionOnEmit: false,
|
||||
);
|
||||
final adapter = _adapter(native: native, source: source);
|
||||
await adapter.start();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
native.published.clear();
|
||||
native.urgentFlags.clear();
|
||||
source.emitCount = 0;
|
||||
|
||||
native.emitConnection(
|
||||
const WatchBridgeConnectionEvent(
|
||||
isReachable: true,
|
||||
requestsResync: true,
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(source.emitCount, 1);
|
||||
expect(native.published.map((projection) => projection.revision), [1]);
|
||||
expect(native.urgentFlags, [true]);
|
||||
await adapter.stop();
|
||||
},
|
||||
);
|
||||
|
||||
test('dispatches watch command and sends ack back to native layer', () async {
|
||||
final native = _FakeWatchBridgeNativeChannel();
|
||||
final ingress = _FakeCommandIngress();
|
||||
@ -89,6 +142,7 @@ void main() {
|
||||
final source = _FakeProjectionSource(_projection(revision: 3));
|
||||
final adapter = _adapter(native: native, source: source);
|
||||
await adapter.start();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
native.published.clear();
|
||||
source.emitCount = 0;
|
||||
|
||||
@ -372,9 +426,10 @@ final class _FakeIds implements IdGenerator {
|
||||
}
|
||||
|
||||
final class _FakeProjectionSource implements WatchProjectionSource {
|
||||
_FakeProjectionSource(this.current);
|
||||
_FakeProjectionSource(this.current, {this.incrementsRevisionOnEmit = true});
|
||||
|
||||
WatchSessionProjection current;
|
||||
final bool incrementsRevisionOnEmit;
|
||||
var emitCount = 0;
|
||||
final _controller = StreamController<WatchSessionProjection>.broadcast();
|
||||
|
||||
@ -392,19 +447,44 @@ final class _FakeProjectionSource implements WatchProjectionSource {
|
||||
@override
|
||||
Future<WatchSessionProjection> emitCurrentProjection() async {
|
||||
emitCount += 1;
|
||||
final nextRevision = incrementsRevisionOnEmit
|
||||
? current.revision + 1
|
||||
: current.revision;
|
||||
current = WatchSessionProjection(
|
||||
deviceSessionId: current.deviceSessionId,
|
||||
revision: current.revision + 1,
|
||||
projectedAtEpochMs: current.projectedAtEpochMs,
|
||||
revision: nextRevision,
|
||||
projectedAtEpochMs: current.projectedAtEpochMs + 1000,
|
||||
expiresAtEpochMs: current.expiresAtEpochMs == 0
|
||||
? 0
|
||||
: current.expiresAtEpochMs + 1000,
|
||||
phase: current.phase,
|
||||
phoneReachable: current.phoneReachable,
|
||||
seriesIndex: current.seriesIndex,
|
||||
seriesTotal: current.seriesTotal,
|
||||
exerciseName: current.exerciseName,
|
||||
programIndex: current.programIndex,
|
||||
exerciseIndex: current.exerciseIndex,
|
||||
setIndex: current.setIndex,
|
||||
passageIndex: current.passageIndex,
|
||||
passageTotal: current.passageTotal,
|
||||
stepIndex: current.stepIndex,
|
||||
stepTotal: current.stepTotal,
|
||||
stepName: current.stepName,
|
||||
stepType: current.stepType,
|
||||
stepTargetValue: current.stepTargetValue,
|
||||
dominantTimer: current.dominantTimer,
|
||||
secondaryTimers: current.secondaryTimers,
|
||||
primaryAction: current.primaryAction,
|
||||
secondaryActions: current.secondaryActions,
|
||||
nextExerciseName: current.nextExerciseName,
|
||||
statusLabel: current.statusLabel,
|
||||
hasManualScore: current.hasManualScore,
|
||||
currentManualScoreValue: current.currentManualScoreValue,
|
||||
canDecrementScore: current.canDecrementScore,
|
||||
manualScoreTargetValue: current.manualScoreTargetValue,
|
||||
manualScoreTargetLabel: current.manualScoreTargetLabel,
|
||||
manualScoreRepsTargetValue: current.manualScoreRepsTargetValue,
|
||||
manualScoreScope: current.manualScoreScope,
|
||||
);
|
||||
_controller.add(current);
|
||||
return current;
|
||||
@ -479,9 +559,15 @@ final class _FakeWatchBridgeNativeChannel implements WatchBridgeNativeChannel {
|
||||
_connections.add(event);
|
||||
}
|
||||
|
||||
final urgentFlags = <bool>[];
|
||||
|
||||
@override
|
||||
Future<void> publishProjection(WatchSessionProjection projection) async {
|
||||
Future<void> publishProjection(
|
||||
WatchSessionProjection projection, {
|
||||
bool urgent = true,
|
||||
}) async {
|
||||
published.add(projection);
|
||||
urgentFlags.add(urgent);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user