fix(discover): refresh content when a server comes online mid-session

This commit is contained in:
edde746
2026-06-12 15:56:09 +02:00
parent f671f367ea
commit b2cb874b73
5 changed files with 68 additions and 14 deletions
+8 -3
View File
@@ -916,19 +916,24 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
// bind in waves on sign-in / profile switch and slow ones
// reconnect after the initial load; without this they stay
// missing from the sidebar until a re-switch or restart.
context.read<MultiServerProvider>().onOnlineServersChanged = provider.syncToOnlineServers;
context.read<MultiServerProvider>().addOnlineServersListener(provider.syncToOnlineServers);
return provider;
},
),
ChangeNotifierProvider(
create: (context) {
final activeProfile = context.read<ActiveProfileProvider>();
return DiscoverProvider(
context.read<MultiServerProvider>(),
final multiServer = context.read<MultiServerProvider>();
final provider = DiscoverProvider(
multiServer,
context.read<HiddenLibrariesProvider>(),
context.read<LibrariesProvider>(),
isProfileBinding: () => activeProfile.isBinding,
);
// Late server connects (reconnect after outage, slow wave)
// refresh discover the same way they refresh libraries.
multiServer.addOnlineServersListener(provider.syncToOnlineServers);
return provider;
},
),
ChangeNotifierProvider(create: (context) => PlaybackStateProvider()),
+19
View File
@@ -73,6 +73,9 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
Set<String> _lastSeenHiddenKeys = {};
List<String> _lastSeenLibraryOrderKeys = const [];
/// Online servers that contributed to the last successful [load] pass.
Set<String> _loadedOnlineServerIds = {};
Future<void>? _inFlightLoad;
bool _hasPendingLoad = false;
@@ -96,6 +99,19 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
/// a background Continue Watching refresh (clamp only).
int get loadGeneration => _loadGeneration;
/// Reload when a server comes online *mid-session* (reconnect, late wave) —
/// its hubs and continue-watching rows are otherwise missing until a manual
/// refresh. During profile binding this is a no-op: servers bind in waves
/// and main_screen primes one [load] when binding settles, so reacting to
/// each wave would multiply the (expensive) hub fan-out at startup.
Future<void> syncToOnlineServers(Set<String> onlineServerIds) {
if (onlineServerIds.isEmpty || isProfileBinding()) return Future<void>.value();
if (_onDeckState == DiscoverLoadState.loaded && _loadedOnlineServerIds.containsAll(onlineServerIds)) {
return Future<void>.value();
}
return load();
}
/// Full load of Continue Watching + hubs. Concurrent calls coalesce into
/// the in-flight pass plus at most one trailing pass (so a request that
/// arrives mid-load still observes its own fresh fetch).
@@ -148,10 +164,13 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
includePlaybackHubs: false,
);
final fetchedFromServerIds = Set<String>.of(_multiServer.onlineServerIds);
final fetchedOnDeck = await onDeckFuture;
if (isDisposed) return;
_applyOnDeck(fetchedOnDeck);
_onDeckState = DiscoverLoadState.loaded;
_loadedOnlineServerIds = fetchedFromServerIds;
_loadGeneration++;
safeNotifyListeners();
unawaited(_syncSystemShelf(_onDeck));
+19 -9
View File
@@ -43,11 +43,19 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
/// Invoked with the current visibility-filtered online server ids whenever
/// the manager's status stream fires (a server connects, reconnects, drops,
/// or its auth state changes). Lets `LibrariesProvider` reload when the
/// online set grows — servers bind in waves and slow ones reconnect after
/// the initial load — without coupling the two providers by type. Wired once
/// in `main.dart`.
void Function(Set<String> onlineServerIds)? onOnlineServersChanged;
/// or its auth state changes). Lets data providers (`LibrariesProvider`,
/// `DiscoverProvider`) reload when the online set grows — servers bind in
/// waves and slow ones reconnect after the initial load — without coupling
/// the providers by type. Wired once per consumer in `main.dart`.
final List<void Function(Set<String> onlineServerIds)> _onlineServersListeners = [];
void addOnlineServersListener(void Function(Set<String> onlineServerIds) listener) {
_onlineServersListeners.add(listener);
}
void removeOnlineServersListener(void Function(Set<String> onlineServerIds) listener) {
_onlineServersListeners.remove(listener);
}
/// Visibility filter applied by the active app profile. `null` means
/// "all servers visible" (no profile restriction); otherwise only server
@@ -148,11 +156,13 @@ class MultiServerProvider extends ChangeNotifier with DisposableChangeNotifierMi
safeNotifyListeners();
// Reload libraries when the online set changes. LibrariesProvider owns
// the "is anything actually new to me?" decision (its loaded set can
// Reload data providers when the online set changes. Each listener owns
// the "is anything actually new to me?" decision (their loaded sets can
// differ from _previousOnlineServerIds after a load error or a profile
// switch that cleared it), so notify unconditionally and let it decide.
onOnlineServersChanged?.call(currentOnline);
// switch that cleared them), so notify unconditionally and let them decide.
for (final listener in List.of(_onlineServersListeners)) {
listener(currentOnline);
}
// Only re-check live TV when a new server came online
if (hasNewServer) {
@@ -286,6 +286,26 @@ void main() {
expect(provider.onDeck.single.id, 'ep-1');
});
test('syncToOnlineServers reloads for mid-session connects only', () async {
aggregation.onDeckResult = () => [_item('a')];
await provider.load();
final onDeckCallsBefore = aggregation.onDeckCalls;
// Same server set → already covered, no fetch.
await provider.syncToOnlineServers({'server_1'});
expect(aggregation.onDeckCalls, onDeckCallsBefore);
// New server mid-session → full reload.
await provider.syncToOnlineServers({'server_1', 'server_2'});
expect(aggregation.onDeckCalls, onDeckCallsBefore + 1);
// During profile binding the startup priming owns loading — waves are
// ignored so the hub fan-out doesn't run once per wave.
isBinding = true;
await provider.syncToOnlineServers({'server_1', 'server_2', 'server_3'});
expect(aggregation.onDeckCalls, onDeckCallsBefore + 1);
});
test('loadGeneration bumps on full loads only', () async {
aggregation.onDeckResult = () => [_item('a')];
final initial = provider.loadGeneration;
@@ -86,7 +86,7 @@ void main() {
test('invokes onOnlineServersChanged with the visibility-filtered online set', () async {
final p = MultiServerProvider(manager, aggregation);
final calls = <Set<String>>[];
p.onOnlineServersChanged = calls.add;
p.addOnlineServersListener(calls.add);
manager.updateServerStatus(ServerId('srv-1'), true);
await Future<void>.delayed(Duration.zero);
@@ -218,7 +218,7 @@ void main() {
test('expected servers become visible when they reconnect', () async {
final p = MultiServerProvider(manager, aggregation);
final onlineCalls = <Set<String>>[];
p.onOnlineServersChanged = onlineCalls.add;
p.addOnlineServersListener(onlineCalls.add);
p.setVisibleServerIds({'srv-1'});
p.setExpectedVisibleServerIds({'srv-1', 'srv-2'});