provider lifecycle hardenings

This commit is contained in:
Fabian Freund
2026-05-06 18:32:45 +02:00
parent d5f977a3d2
commit fee15cb8d2
25 changed files with 246 additions and 94 deletions
@@ -39,6 +39,10 @@ class WebExtensionsState extends _$WebExtensionsState {
: _imageCache = LRUCache(50, onEvict: (image) => image.dispose());
void _onExtensionUpdate(ExtensionDataEvent event) {
if (!ref.mounted) {
return;
}
final ExtensionDataEvent(:extensionId, :data) = event;
if (data != null) {
@@ -81,6 +85,11 @@ class WebExtensionsState extends _$WebExtensionsState {
final image = await tryDecodeImage(bytes);
if (!ref.mounted) {
image?.dispose();
return;
}
if (image != null) {
// set() will evict the old entry via onEvict callback, which handles disposal
_imageCache.set(extensionId, image);
@@ -185,13 +194,13 @@ class WebExtensionsState extends _$WebExtensionsState {
},
);
ref.onDispose(() async {
ref.onDispose(() {
// Dispose all cached images
_imageCache.clear();
// Cancel all stream subscriptions
for (final sub in subscriptions) {
await sub.cancel();
unawaited(sub.cancel());
}
});
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
}
String _$webExtensionsStateHash() =>
r'13cbbea409b5b643eb6766689f6b23a4d1701204';
r'e067323e9a0a466e46e0c4d529667950ee62d4ca';
final class WebExtensionsStateFamily extends $Family
with
@@ -784,8 +784,9 @@ class TabRepository extends _$TabRepository {
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen(
(event) async {
if (event.tabId != null) {
final tabState = ref.read(tabStatesProvider)[event.tabId];
final tabId = event.tabId;
if (tabId != null) {
final tabState = ref.read(tabStatesProvider)[tabId];
if (tabState != null) {
final uri = Uri.parse(event.url);
final originUri = event.originUrl.mapNotNull(Uri.parse);
@@ -793,52 +794,81 @@ class TabRepository extends _$TabRepository {
final targetContainerId = await ref
.read(containerRepositoryProvider.notifier)
.siteAssignedContainerId(Uri.parse(uri.origin));
if (!ref.mounted) {
return;
}
final containerData = await targetContainerId.mapNotNull(
(id) => ref
.read(containerRepositoryProvider.notifier)
.getContainerData(id),
);
if (!ref.mounted) {
return;
}
if (containerData != null) {
final currentTabState = ref.read(tabStatesProvider)[tabId];
if (currentTabState == null) {
logger.w('Could not get tab for assignement ${event.url}');
return;
}
final tabIsEmpty =
tabState.url == TabState.defaultUrl &&
tabState.historyState.items.isEmpty;
currentTabState.url == TabState.defaultUrl &&
currentTabState.historyState.items.isEmpty;
if (event.blocked || tabIsEmpty) {
await addTab(
url: uri,
tabMode: tabState.tabMode,
tabMode: currentTabState.tabMode,
containerSelection: TabContainerSelection.specific(
containerData,
),
parentId: tabState.id,
parentId: currentTabState.id,
selectTab: true,
);
if (tabState.historyState.items.isEmpty) {
await closeTab(tabState.id);
if (!ref.mounted) {
return;
}
if (currentTabState.historyState.items.isEmpty) {
await closeTab(currentTabState.id);
}
} else {
final tabContainerId = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabContainerId(tabState.id);
.getTabContainerId(currentTabState.id);
if (!ref.mounted) {
return;
}
final latestTabState = ref.read(tabStatesProvider)[tabId];
if (latestTabState == null) {
logger.w('Could not get tab for assignement ${event.url}');
return;
}
if (targetContainerId != tabContainerId) {
if (originUri == null) {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(tabState.id, containerData);
} else if (tabState.url == originUri) {
.assignContainer(latestTabState.id, containerData);
} else if (latestTabState.url == originUri) {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(
tabState.id,
latestTabState.id,
containerData,
closeOldTab: false,
);
} else {
logger.w(
'Could not match origin url for assignment ${tabState.url} to request ${event.originUrl}',
'Could not match origin url for assignment ${latestTabState.url} to request ${event.originUrl}',
);
}
}
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
}
}
String _$tabRepositoryHash() => r'a68941d373f348201e3d8b1cab26343acca3852f';
String _$tabRepositoryHash() => r'f458baaf9061143a668470dccd57f32664728951';
abstract class _$TabRepository extends $Notifier<void> {
void build();
@@ -42,20 +42,24 @@ class AllowUnsignedExtensions extends _$AllowUnsignedExtensions {
await fixator.unregister(_signatureRequiredPref);
await GeckoPrefService().applyPrefs({_signatureRequiredPref: true});
}
if (!ref.mounted) {
return;
}
state = AsyncData(allow);
}
@override
FutureOr<bool> build() async {
final fixator = ref.read(preferenceFixatorProvider.notifier);
final prefs = await GeckoPrefService().getPrefs([_signatureRequiredPref]);
final pref = prefs[_signatureRequiredPref];
final allowUnsigned = pref?.value == false;
// Re-register with fixator to prevent Gecko from resetting
if (allowUnsigned) {
await ref
.read(preferenceFixatorProvider.notifier)
.register(_signatureRequiredPref, false);
await fixator.register(_signatureRequiredPref, false);
}
return allowUnsigned;
@@ -68,6 +72,10 @@ class AddonAutoUpdate extends _$AddonAutoUpdate {
final service = ref.read(addonServiceProvider);
await service.setAddonAutoUpdateEnabled(enabled: enabled);
if (!ref.mounted) {
return;
}
state = AsyncData(enabled);
}
@@ -34,7 +34,7 @@ final class AllowUnsignedExtensionsProvider
}
String _$allowUnsignedExtensionsHash() =>
r'3b67f51cabf8e2f37d992320c5b93bb61c6dbe1f';
r'ba282219edc9149da886371d873d2a5ee0dfb6dd';
abstract class _$AllowUnsignedExtensions extends $AsyncNotifier<bool> {
FutureOr<bool> build();
@@ -78,7 +78,7 @@ final class AddonAutoUpdateProvider
AddonAutoUpdate create() => AddonAutoUpdate();
}
String _$addonAutoUpdateHash() => r'89791e8b771da715b068bbdbe5c3c24a3dad4194';
String _$addonAutoUpdateHash() => r'0a2661316d1d1345e81df2552925eda3c2bf6ec1';
abstract class _$AddonAutoUpdate extends $AsyncNotifier<bool> {
FutureOr<bool> build();
@@ -31,11 +31,16 @@ class PreferenceChangeListener extends _$PreferenceChangeListener {
Stream<GeckoPref> build() async* {
final events = ref.watch(eventServiceProvider);
ref.onDispose(() {
unawaited(GeckoPrefService().stopObserveChanges());
});
await GeckoPrefService().startObserveChanges();
ref.onDispose(() async {
if (!ref.mounted) {
await GeckoPrefService().stopObserveChanges();
});
return;
}
yield* events.prefUpdateEvent;
}
@@ -34,7 +34,7 @@ final class PreferenceChangeListenerProvider
}
String _$preferenceChangeListenerHash() =>
r'de9759b32a6a6b9bab5656137a365da78633f866';
r'baf2abe1948998a043a8473547a225582dbc8c7b';
abstract class _$PreferenceChangeListener extends $StreamNotifier<GeckoPref> {
Stream<GeckoPref> build();
@@ -243,8 +243,8 @@ class _PreferenceRepository extends _$PreferenceRepository {
_prefSubject = BehaviorSubject<Map<String, GeckoPref>>();
ref.onDispose(() async {
await _prefSubject.close();
ref.onDispose(() {
unawaited(_prefSubject.close());
});
ref.onAddListener(() {
@@ -260,19 +260,21 @@ class _PreferenceRepository extends _$PreferenceRepository {
@Riverpod()
class UnifiedPreferenceSettingsRepository
extends _$UnifiedPreferenceSettingsRepository {
Map<String, PreferenceSettingGroup>? _statelessGroups;
Future<void> apply() async {
final preferenceRepository = ref.read(
_preferenceRepositoryProvider(partition).notifier,
);
_statelessGroups = await ref.read(
final groups = await ref.read(
_preferenceSettingGroupsProvider(partition).future,
);
if (!ref.mounted) {
return;
}
final prefs = {
for (final group in _statelessGroups!.values)
for (final group in groups.values)
...Map.fromEntries(
group.settings.entries
.where((e) => !e.value.requireUserOptIn)
@@ -288,11 +290,15 @@ class UnifiedPreferenceSettingsRepository
_preferenceRepositoryProvider(partition).notifier,
);
_statelessGroups = await ref.read(
final groups = await ref.read(
_preferenceSettingGroupsProvider(partition).future,
);
final prefs = _statelessGroups!.values
if (!ref.mounted) {
return;
}
final prefs = groups.values
.map((group) => group.settings.keys)
.flattened
.toList();
@@ -304,14 +310,13 @@ class UnifiedPreferenceSettingsRepository
Stream<Map<String, PreferenceSettingGroup>> build(
PreferencePartition partition,
) async* {
_statelessGroups = await ref.watch(
final prefStream = ref.watch(_preferenceRepositoryProvider(partition));
final groups = await ref.watch(
_preferenceSettingGroupsProvider(partition).future,
);
final prefStream = ref.watch(_preferenceRepositoryProvider(partition));
yield* prefStream.map(
(prefs) => _statelessGroups!.map(
(prefs) => groups.map(
(groupName, group) => MapEntry(
groupName,
group.copyWith.settings(
@@ -329,26 +334,28 @@ class UnifiedPreferenceSettingsRepository
@Riverpod()
class PreferenceSettingsGroupRepository
extends _$PreferenceSettingsGroupRepository {
PreferenceSettingGroup? _statelessSettingGroup;
Future<void> apply({List<String>? filter}) async {
final preferenceRepository = ref.read(
_preferenceRepositoryProvider(partition).notifier,
);
_statelessSettingGroup ??= await ref.read(
final settingGroup = await ref.read(
_preferenceSettingGroupProvider(partition, groupName).future,
);
if (!ref.mounted) {
return;
}
final prefs = Map.fromEntries(
filter?.map(
(e) => MapEntry(
e,
_statelessSettingGroup!.settings[e]?.value ??
settingGroup.settings[e]?.value ??
(throw Exception('Preference not part of group')),
),
) ??
_statelessSettingGroup!.settings.entries
settingGroup.settings.entries
.where((e) => !e.value.requireUserOptIn)
.map((e) => MapEntry(e.key, e.value.value)),
);
@@ -361,19 +368,21 @@ class PreferenceSettingsGroupRepository
_preferenceRepositoryProvider(partition).notifier,
);
_statelessSettingGroup ??= await ref.read(
final settingGroup = await ref.read(
_preferenceSettingGroupProvider(partition, groupName).future,
);
if (!ref.mounted) {
return;
}
if (filter != null &&
filter.any(
(value) => !_statelessSettingGroup!.settings.containsKey(value),
)) {
filter.any((value) => !settingGroup.settings.containsKey(value))) {
throw Exception('Preference not part of group');
}
await preferenceRepository.resetPrefs(
filter ?? _statelessSettingGroup!.settings.keys.toList(),
filter ?? settingGroup.settings.keys.toList(),
);
}
@@ -382,15 +391,14 @@ class PreferenceSettingsGroupRepository
PreferencePartition partition,
String groupName,
) async* {
_statelessSettingGroup = await ref.watch(
final prefStream = ref.watch(_preferenceRepositoryProvider(partition));
final settingGroup = await ref.watch(
_preferenceSettingGroupProvider(partition, groupName).future,
);
final prefStream = ref.watch(_preferenceRepositoryProvider(partition));
yield* prefStream.map(
(prefs) => _statelessSettingGroup!.copyWith.settings(
_statelessSettingGroup!.settings.map(
(prefs) => settingGroup.copyWith.settings(
settingGroup.settings.map(
(key, value) => MapEntry(key, value.copyWith.current(prefs[key])),
),
),
@@ -334,7 +334,7 @@ final class _PreferenceRepositoryProvider
}
String _$_preferenceRepositoryHash() =>
r'c416d7684f1e8f24dd9e5fc98ecb6312c07d938f';
r'ca5ccbe1c3f5f1576e3e7ba359c3cd1324281a97';
final class _PreferenceRepositoryFamily extends $Family
with
@@ -441,7 +441,7 @@ final class UnifiedPreferenceSettingsRepositoryProvider
}
String _$unifiedPreferenceSettingsRepositoryHash() =>
r'03c8c2f247974ff32bba14b4b34857a0f14f6f37';
r'c544b5815ec676ddb6cde1ffeb8c9ef4a7dc4469';
final class UnifiedPreferenceSettingsRepositoryFamily extends $Family
with
@@ -554,7 +554,7 @@ final class PreferenceSettingsGroupRepositoryProvider
}
String _$preferenceSettingsGroupRepositoryHash() =>
r'b5c439643ec78094122c33983e58d0977641832a';
r'c8335b39c4c9fa95c4d415231b103a2feff97d26';
final class PreferenceSettingsGroupRepositoryFamily extends $Family
with
@@ -44,6 +44,7 @@ Future<void> showPwaInstallDialog(BuildContext context, WidgetRef ref) async {
url: url,
);
if (!context.mounted) return;
if (config == null) return;
final name = config.name;
@@ -106,6 +107,7 @@ Future<void> showShortcutInstallDialog(
showAppOption: showAppOption,
);
if (!context.mounted) return;
if (config == null) return;
final name = config.name;
@@ -34,7 +34,15 @@ class ReaderableScreenController extends _$ReaderableScreenController {
final eventChange = ref.read(eventServiceProvider).readerableEvents.first;
final toggle = _service.toggleReaderView(enable);
state = await AsyncValue.guard(() => Future.wait([toggle, eventChange]));
final result = await AsyncValue.guard(
() => Future.wait([toggle, eventChange]),
);
if (!ref.mounted) {
return;
}
state = result;
}
@override
@@ -35,7 +35,7 @@ final class ReaderableScreenControllerProvider
}
String _$readerableScreenControllerHash() =>
r'62b2167ff8f9fe21c3cf3339e135a50aa684160c';
r'46206cfb431c5d36dc6351a8fc52c3b98b86dd0f';
abstract class _$ReaderableScreenController extends $AsyncNotifier<void> {
FutureOr<void> build();
@@ -38,7 +38,7 @@ class TabSearchRepository extends _$TabSearchRepository {
String ellipsis = '',
}) async {
if (input.isNotEmpty) {
state = await AsyncValue.guard(() async {
final result = await AsyncValue.guard(() async {
return (
query: input,
results: await ref
@@ -55,6 +55,12 @@ class TabSearchRepository extends _$TabSearchRepository {
.get(),
);
});
if (!ref.mounted) {
return;
}
state = result;
} else {
state = const AsyncValue.data(null);
}
@@ -55,7 +55,7 @@ final class TabSearchRepositoryProvider
}
String _$tabSearchRepositoryHash() =>
r'473057d6a9f1e76d6b7fec13f710189d758bc924';
r'a05922f7c487105f46abbd272fbc2ed72994c332';
final class TabSearchRepositoryFamily extends $Family
with