From 29c029f04a81cc00e2ef521e8e571cdaadf246e6 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:25:48 +0200 Subject: [PATCH] refactor(watch-together): share core primitives --- lib/watch_together/models/playback_state.dart | 5 ++--- lib/watch_together/primitives.dart | 9 +++++++++ .../services/attached_player.dart | 5 ++--- lib/watch_together/services/clock_sync.dart | 5 ++--- .../services/guest_playback_reconciler.dart | 15 +++------------ .../services/host_playback_coordinator.dart | 15 +++------------ .../services/watch_together_controller.dart | 5 ++--- test/watch_together/primitives_test.dart | 19 +++++++++++++++++++ 8 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 lib/watch_together/primitives.dart create mode 100644 test/watch_together/primitives_test.dart diff --git a/lib/watch_together/models/playback_state.dart b/lib/watch_together/models/playback_state.dart index de758af4..abf98bc5 100644 --- a/lib/watch_together/models/playback_state.dart +++ b/lib/watch_together/models/playback_state.dart @@ -1,5 +1,4 @@ -import 'package:collection/collection.dart'; - +import '../primitives.dart'; import 'watch_session.dart'; /// Playback lifecycle phase broadcast by the host. @@ -146,7 +145,7 @@ class PlaybackState { other.anchorHostTimeMs == anchorHostTimeMs && other.rate == rate && other.controlMode == controlMode && - const ListEquality().equals(other.waitingOn, waitingOn) && + orderedStringListsEqual(other.waitingOn, waitingOn) && other.actorPeerId == actorPeerId && other.actionHint == actionHint; diff --git a/lib/watch_together/primitives.dart b/lib/watch_together/primitives.dart new file mode 100644 index 00000000..299adbb8 --- /dev/null +++ b/lib/watch_together/primitives.dart @@ -0,0 +1,9 @@ +int watchTogetherSystemNowMs() => DateTime.now().millisecondsSinceEpoch; + +bool orderedStringListsEqual(List first, List second) { + if (first.length != second.length) return false; + for (var i = 0; i < first.length; i++) { + if (first[i] != second[i]) return false; + } + return true; +} diff --git a/lib/watch_together/services/attached_player.dart b/lib/watch_together/services/attached_player.dart index 154d44dd..eb113028 100644 --- a/lib/watch_together/services/attached_player.dart +++ b/lib/watch_together/services/attached_player.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../../mpv/mpv.dart'; import '../../utils/app_logger.dart'; +import '../primitives.dart'; enum _ExpectationKind { playing, rate } @@ -44,7 +45,7 @@ class _Expectation { class AttachedPlayer { AttachedPlayer({required Player player, required this._onLost, this._remoteSeek, int Function()? nowMs}) : _player = player, - _nowMs = nowMs ?? _systemNowMs { + _nowMs = nowMs ?? watchTogetherSystemNowMs { _lastPlaying = player.state.playing; _lastBuffering = player.state.buffering; _lastRate = player.state.rate; @@ -59,8 +60,6 @@ class AttachedPlayer { ); } - static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch; - /// How long an issued command may wait for its property event before the /// expectation is considered dead (covers silently-swallowed commands). static const int _expectationTtlMs = 3000; diff --git a/lib/watch_together/services/clock_sync.dart b/lib/watch_together/services/clock_sync.dart index c88c77e3..fca552fd 100644 --- a/lib/watch_together/services/clock_sync.dart +++ b/lib/watch_together/services/clock_sync.dart @@ -1,6 +1,7 @@ import 'dart:async'; import '../../utils/app_logger.dart'; +import '../primitives.dart'; /// NTP-style clock-offset estimation against the session host (guest side). /// @@ -12,9 +13,7 @@ import '../../utils/app_logger.dart'; /// All time reads go through the injected [nowMs] so tests can virtualize /// time alongside `fakeAsync`. class ClockSync { - ClockSync({required this._sendPing, int Function()? nowMs}) : _nowMs = nowMs ?? _systemNowMs; - - static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch; + ClockSync({required this._sendPing, int Function()? nowMs}) : _nowMs = nowMs ?? watchTogetherSystemNowMs; static const int _windowSize = 8; static const int _maxAcceptedRttMs = 1000; diff --git a/lib/watch_together/services/guest_playback_reconciler.dart b/lib/watch_together/services/guest_playback_reconciler.dart index b3bb56d1..89a0ce56 100644 --- a/lib/watch_together/services/guest_playback_reconciler.dart +++ b/lib/watch_together/services/guest_playback_reconciler.dart @@ -4,6 +4,7 @@ import '../../utils/app_logger.dart'; import '../models/playback_state.dart'; import '../models/sync_message.dart'; import '../models/watch_session.dart'; +import '../primitives.dart'; import 'attached_player.dart'; import 'clock_sync.dart'; @@ -52,9 +53,7 @@ class GuestPlaybackReconciler { this._callbacks = const GuestReconcilerCallbacks(), int Function()? nowMs, }) : _clock = clockSync, - _nowMs = nowMs ?? _systemNowMs; - - static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch; + _nowMs = nowMs ?? watchTogetherSystemNowMs; // Tuning constants. static const int tickMs = 500; @@ -238,7 +237,7 @@ class GuestPlaybackReconciler { _reportedPhase = state.phase; _callbacks.onPhaseChanged?.call(state.phase); } - if (!_listEquals(state.waitingOn, _reportedWaitingOn)) { + if (!orderedStringListsEqual(state.waitingOn, _reportedWaitingOn)) { _reportedWaitingOn = state.waitingOn; _callbacks.onWaitingOnChanged?.call(state.waitingOn); } @@ -585,12 +584,4 @@ class GuestPlaybackReconciler { _lastSentStatus = status; _sendToHost(SyncMessage.status(status, peerId: myPeerId)); } - - static bool _listEquals(List a, List b) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; - } - return true; - } } diff --git a/lib/watch_together/services/host_playback_coordinator.dart b/lib/watch_together/services/host_playback_coordinator.dart index 9bf360f0..92f45690 100644 --- a/lib/watch_together/services/host_playback_coordinator.dart +++ b/lib/watch_together/services/host_playback_coordinator.dart @@ -4,6 +4,7 @@ import 'dart:math'; import '../../utils/app_logger.dart'; import '../models/playback_state.dart'; import '../models/watch_session.dart'; +import '../primitives.dart'; import 'attached_player.dart'; /// Callbacks the coordinator surfaces to the provider/UI layer. @@ -45,9 +46,7 @@ class HostPlaybackCoordinator { required this._sendState, this._callbacks = const HostCoordinatorCallbacks(), int Function()? nowMs, - }) : _nowMs = nowMs ?? _systemNowMs; - - static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch; + }) : _nowMs = nowMs ?? watchTogetherSystemNowMs; // Tuning constants. static const int stallGraceMs = 500; @@ -785,18 +784,10 @@ class HostPlaybackCoordinator { if (toPeerId == null) { final previousWaiting = _lastBroadcast?.waitingOn ?? const []; _lastBroadcast = state; - if (!_listEquals(previousWaiting, waitingOn)) { + if (!orderedStringListsEqual(previousWaiting, waitingOn)) { _callbacks.onWaitingOnChanged?.call(waitingOn); } } _sendState(state, toPeerId: toPeerId); } - - static bool _listEquals(List a, List b) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; - } - return true; - } } diff --git a/lib/watch_together/services/watch_together_controller.dart b/lib/watch_together/services/watch_together_controller.dart index 57fb5e15..0ab963d1 100644 --- a/lib/watch_together/services/watch_together_controller.dart +++ b/lib/watch_together/services/watch_together_controller.dart @@ -5,6 +5,7 @@ import '../../utils/app_logger.dart'; import '../models/playback_state.dart'; import '../models/sync_message.dart'; import '../models/watch_session.dart'; +import '../primitives.dart'; import 'attached_player.dart'; import 'clock_sync.dart'; import 'guest_playback_reconciler.dart'; @@ -28,7 +29,7 @@ class WatchTogetherController { int Function()? nowMs, }) : _peerService = peerService, _session = session, - _nowMs = nowMs ?? _systemNowMs { + _nowMs = nowMs ?? watchTogetherSystemNowMs { if (session.isHost) { _coordinator = HostPlaybackCoordinator( myPeerId: peerService.myPeerId ?? '', @@ -65,8 +66,6 @@ class WatchTogetherController { _subscriptions.add(peerService.onPeerDisconnected.listen(_handlePeerDisconnected)); } - static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch; - final WatchTogetherPeerService _peerService; final int Function() _nowMs; WatchSession _session; diff --git a/test/watch_together/primitives_test.dart b/test/watch_together/primitives_test.dart new file mode 100644 index 00000000..d86c2d3f --- /dev/null +++ b/test/watch_together/primitives_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/watch_together/primitives.dart'; + +void main() { + test('orderedStringListsEqual preserves order and multiplicity', () { + expect(orderedStringListsEqual(const ['a', 'b'], const ['a', 'b']), isTrue); + expect(orderedStringListsEqual(const ['a'], const ['a', 'b']), isFalse); + expect(orderedStringListsEqual(const ['a', 'b'], const ['b', 'a']), isFalse); + expect(orderedStringListsEqual(const ['a', 'a'], const ['a', 'b']), isFalse); + }); + + test('watchTogetherSystemNowMs returns wall-clock milliseconds', () { + final before = DateTime.now().millisecondsSinceEpoch; + final value = watchTogetherSystemNowMs(); + final after = DateTime.now().millisecondsSinceEpoch; + + expect(value, inInclusiveRange(before, after)); + }); +}