fix(discover): avoid progress refresh fanout
This commit is contained in:
@@ -24,12 +24,12 @@ enum DiscoverLoadState { initial, loading, loaded, error }
|
||||
|
||||
/// Owns the Discover tab's data: the Continue Watching row and the home hub
|
||||
/// list, including the refresh policy that used to live in the screen —
|
||||
/// watch events refresh only Continue Watching (one on-deck call, zero hub
|
||||
/// refetches), deletions drop the item from every visible list in place and
|
||||
/// then refresh only Continue Watching, hidden-library changes trigger a
|
||||
/// full reload, library-order changes re-sort hubs in place without
|
||||
/// refetching, and the platform launcher shelf syncs from every on-deck
|
||||
/// update.
|
||||
/// durable watch events refresh only Continue Watching (one on-deck call,
|
||||
/// zero hub refetches), playback progress patches the visible row in place,
|
||||
/// deletions drop the item from every visible list in place and then refresh
|
||||
/// only Continue Watching, hidden-library changes trigger a full reload,
|
||||
/// library-order changes re-sort hubs in place without refetching, and the
|
||||
/// platform launcher shelf syncs from every on-deck update.
|
||||
///
|
||||
/// Lives inside the profile-keyed provider subtree, so a profile switch
|
||||
/// resets it by construction. The screen is a consumer: it renders this
|
||||
@@ -476,6 +476,16 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
|
||||
}
|
||||
|
||||
void _onWatchStateChanged(WatchStateEvent event) {
|
||||
if (event.changeType == WatchStateChangeType.progressUpdate && event.isNowWatched != true) {
|
||||
final viewOffset = event.viewOffset;
|
||||
final index = _onDeck.indexWhere((item) => item.globalKey == event.globalKey);
|
||||
if (viewOffset != null && index != -1 && _onDeck[index].viewOffsetMs != viewOffset) {
|
||||
_onDeck = List.of(_onDeck)..[index] = _onDeck[index].copyWith(viewOffsetMs: viewOffset);
|
||||
safeNotifyListeners();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.changeType == WatchStateChangeType.removedFromContinueWatching) {
|
||||
final remaining = _onDeck.where((item) => item.id != event.itemId).toList();
|
||||
if (remaining.length != _onDeck.length) {
|
||||
|
||||
@@ -54,8 +54,9 @@ MediaHub _hub(
|
||||
);
|
||||
|
||||
/// Counting fake — the provider's fetch-cost policy is the contract under
|
||||
/// test: a watch event must cost exactly one on-deck call and zero hub
|
||||
/// refetches, an order change zero calls, a hidden-set change one full pass.
|
||||
/// test: a durable watch event must cost exactly one on-deck call and zero hub
|
||||
/// refetches, progress zero calls, an order change zero calls, and a hidden-set
|
||||
/// change one full pass.
|
||||
class _FakeAggregationService extends DataAggregationService {
|
||||
_FakeAggregationService(super.serverManager);
|
||||
|
||||
@@ -198,7 +199,7 @@ void main() {
|
||||
expect(provider.hubs.map((h) => h.id), ['keep']);
|
||||
});
|
||||
|
||||
test('watch event refreshes continue watching with one call and zero hub refetches', () async {
|
||||
test('watched and unwatched events refresh continue watching only', () async {
|
||||
aggregation.onDeckResult = () => [_item('ep-1', parentId: 'season-1')];
|
||||
aggregation.hubsResult = () => [_hub('hub-1')];
|
||||
await provider.load();
|
||||
@@ -210,11 +211,53 @@ void main() {
|
||||
|
||||
expect(aggregation.onDeckCalls, onDeckCallsBefore + 1);
|
||||
expect(aggregation.hubCalls, hubCallsBefore);
|
||||
|
||||
WatchStateNotifier().notifyWatched(item: _item('ep-1', parentId: 'season-1'), isNowWatched: false);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(aggregation.onDeckCalls, onDeckCallsBefore + 2);
|
||||
expect(aggregation.hubCalls, hubCallsBefore);
|
||||
});
|
||||
|
||||
test('sub-threshold progress patches the row without refetching', () async {
|
||||
final playing = _item('ep-1').copyWith(durationMs: 100000, viewOffsetMs: 10000, viewCount: 0);
|
||||
aggregation.onDeckResult = () => [playing, for (var i = 2; i <= 21; i++) _item('ep-$i')];
|
||||
aggregation.hubsResult = () => [_hub('hub-1')];
|
||||
await provider.load();
|
||||
final onDeckCallsBefore = aggregation.onDeckCalls;
|
||||
final hubCallsBefore = aggregation.hubCalls;
|
||||
|
||||
WatchStateNotifier().notifyProgress(item: playing, viewOffset: 30000, duration: 100000);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(provider.onDeck.first.viewOffsetMs, 30000);
|
||||
expect(provider.onDeck.first.isWatched, isFalse);
|
||||
expect(provider.onDeck, hasLength(DiscoverProvider.continueWatchingPreviewLimit));
|
||||
expect(provider.hasMoreContinueWatching, isTrue);
|
||||
expect(aggregation.onDeckCalls, onDeckCallsBefore);
|
||||
expect(aggregation.hubCalls, hubCallsBefore);
|
||||
});
|
||||
|
||||
test('watched-threshold progress refreshes continue watching only', () async {
|
||||
final playing = _item('ep-1').copyWith(durationMs: 100000, viewOffsetMs: 80000);
|
||||
aggregation.onDeckResult = () => [playing];
|
||||
aggregation.hubsResult = () => [_hub('hub-1')];
|
||||
await provider.load();
|
||||
final onDeckCallsBefore = aggregation.onDeckCalls;
|
||||
final hubCallsBefore = aggregation.hubCalls;
|
||||
|
||||
WatchStateNotifier().notifyProgress(item: playing, viewOffset: 95000, duration: 100000);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(aggregation.onDeckCalls, onDeckCallsBefore + 1);
|
||||
expect(aggregation.hubCalls, hubCallsBefore);
|
||||
});
|
||||
|
||||
test('removal event drops the row immediately, then refreshes in background', () async {
|
||||
aggregation.onDeckResult = () => [_item('ep-1'), _item('ep-2')];
|
||||
await provider.load();
|
||||
final onDeckCallsBefore = aggregation.onDeckCalls;
|
||||
final hubCallsBefore = aggregation.hubCalls;
|
||||
|
||||
var sawImmediateRemoval = false;
|
||||
provider.addListener(() {
|
||||
@@ -229,6 +272,8 @@ void main() {
|
||||
|
||||
expect(sawImmediateRemoval, isTrue);
|
||||
expect(provider.onDeck.map((i) => i.id), ['ep-2']);
|
||||
expect(aggregation.onDeckCalls, onDeckCallsBefore + 1);
|
||||
expect(aggregation.hubCalls, hubCallsBefore);
|
||||
});
|
||||
|
||||
test('deletion drops the item from on-deck and hubs, then refreshes continue watching only', () async {
|
||||
|
||||
Reference in New Issue
Block a user