Move deletions to foreground

This commit is contained in:
franzap
2026-04-08 14:35:48 -03:00
parent d3687ff308
commit 227fb10d79
3 changed files with 74 additions and 48 deletions
@@ -188,47 +188,6 @@ Future<bool> _checkForUpdatesInBackground(Set<String>? appCatalogRelays) async {
subscriptionPrefix: 'app-bg',
);
// NIP-09: fetch kind-5 deletion events since last sync.
// No author filter — the AppCatalog relay is curated and the `since`
// cursor keeps incremental runs cheap. The DB layer auto-processes
// each kind-5 event (removes referenced events, records in deletions
// table). We then clean up the kind-5 events themselves as housekeeping,
// but save the cursor regardless — a cleanup failure is non-critical.
final secureStorage = SecureStorageService();
final lastDeletionSync = await secureStorage.getDeletionsSyncedUntil();
final deletionRequests = await storage.query(
RequestFilter<EventDeletionRequest>(
since: lastDeletionSync,
limit: 99,
).toRequest(),
source: const RemoteSource(relays: 'AppCatalog', stream: false),
subscriptionPrefix: 'app-bg-deletions',
);
await secureStorage.setDeletionsSyncedUntil(DateTime.now());
if (deletionRequests.isNotEmpty) {
// Purplebase's NIP-09 processing only deletes events whose pubkey
// matches the kind-5 author. Relay-authority deletions (blacklists)
// are signed by zapstore/community keys, not the app author, so
// those targets survive. Collect them and delete directly by ID.
const trustedAuthorities = {kZapstorePubkey, kZapstoreCommunityPubkey};
final authorityTargetIds = deletionRequests
.where((dr) => trustedAuthorities.contains(dr.event.pubkey))
.expand((dr) => dr.deletedEventIds)
.toSet();
try {
final storageNotifier = storage as PurplebaseStorageNotifier;
await storageNotifier
.delete(deletionRequests.map((d) => d.id).toSet());
if (authorityTargetIds.isNotEmpty) {
await storageNotifier.delete(authorityTargetIds);
}
} catch (_) {
// Orphaned kind-5 rows are harmless; referenced events are
// already removed by the DB layer during save.
}
}
final updatableInstallables = <String, Installable>{};
for (final entry in catalog.installableByApp.entries) {
if (packageManager.hasUpdate(entry.key, entry.value)) {
+51
View File
@@ -0,0 +1,51 @@
import 'package:models/models.dart';
import 'package:purplebase/purplebase.dart';
import 'package:zapstore/constants/app_constants.dart';
import 'package:zapstore/services/secure_storage_service.dart';
/// Fetches NIP-09 kind-5 deletion events from the AppCatalog relay since the
/// last sync cursor, applies them to local storage, and advances the cursor.
///
/// The AppCatalog relay is curated, so no author filter is needed. The `since`
/// cursor (persisted in secure storage) keeps incremental runs cheap.
///
/// Purplebase auto-processes each kind-5 event on save (removes referenced
/// events whose pubkey matches the kind-5 author). However, relay-authority
/// deletions (blacklists signed by zapstore/community keys) won't match the
/// original author — those targets are deleted explicitly here.
Future<void> processDeletions({
required PurplebaseStorageNotifier storage,
required SecureStorageService secureStorage,
required String subscriptionPrefix,
}) async {
final lastSync = await secureStorage.getDeletionsSyncedUntil();
final deletionRequests = await storage.query(
RequestFilter<EventDeletionRequest>(
since: lastSync,
limit: 99,
).toRequest(),
source: const RemoteSource(relays: 'AppCatalog', stream: false),
subscriptionPrefix: subscriptionPrefix,
);
await secureStorage.setDeletionsSyncedUntil(DateTime.now());
if (deletionRequests.isEmpty) return;
const trustedAuthorities = {kZapstorePubkey, kZapstoreCommunityPubkey};
final authorityTargetIds = deletionRequests
.where((dr) => trustedAuthorities.contains(dr.event.pubkey))
.expand((dr) => dr.deletedEventIds)
.toSet();
try {
await storage.delete(deletionRequests.map((d) => d.id).toSet());
if (authorityTargetIds.isNotEmpty) {
await storage.delete(authorityTargetIds);
}
} catch (_) {
// Orphaned kind-5 rows are harmless; referenced events are
// already removed by the DB layer during save.
}
}
+23 -7
View File
@@ -4,8 +4,11 @@ import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/main.dart';
import 'package:purplebase/purplebase.dart';
import 'package:zapstore/services/catalog_fetcher.dart';
import 'package:zapstore/services/deletion_processor.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/services/secure_storage_service.dart';
import 'package:zapstore/utils/extensions.dart';
/// How often to poll for updates from remote relays
@@ -143,13 +146,22 @@ class UpdatePollerNotifier extends StateNotifier<UpdatePollerState> {
return;
}
final storage =
ref.read(storageNotifierProvider.notifier) as PurplebaseStorageNotifier;
final result = await fetchCatalog(
storage: ref.read(storageNotifierProvider.notifier),
storage: storage,
installedIds: pmState.installed.keys.toSet(),
platform: ref.read(packageManagerProvider.notifier).platform,
subscriptionPrefix: 'app-updates-poll',
);
await processDeletions(
storage: storage,
secureStorage: ref.read(secureStorageServiceProvider),
subscriptionPrefix: 'app-deletions-poll',
);
state = state.copyWith(catalogedIds: result.catalogedIds);
}
@@ -211,8 +223,9 @@ final categorizedUpdatesProvider = Provider<CategorizedUpdates>((ref) {
}
final catalogedIds = pollerState.catalogedIds;
final catalogedInstalledIds =
installed.keys.where(catalogedIds.contains).toSet();
final catalogedInstalledIds = installed.keys
.where(catalogedIds.contains)
.toSet();
if (catalogedInstalledIds.isEmpty) {
return CategorizedUpdates(
@@ -221,9 +234,9 @@ final categorizedUpdatesProvider = Provider<CategorizedUpdates>((ref) {
upToDateApps: const [],
uncatalogedApps: installed.values.toList()
..sort(
(a, b) => (a.name ?? a.appId)
.toLowerCase()
.compareTo((b.name ?? b.appId).toLowerCase()),
(a, b) => (a.name ?? a.appId).toLowerCase().compareTo(
(b.name ?? b.appId).toLowerCase(),
),
),
);
}
@@ -236,7 +249,10 @@ final categorizedUpdatesProvider = Provider<CategorizedUpdates>((ref) {
// the same path used by the detail screen and install button.
final appsState = ref.watch(
query<App>(
tags: {'#d': catalogedInstalledIds, '#f': {platform}},
tags: {
'#d': catalogedInstalledIds,
'#f': {platform},
},
and: (app) => {
app.latestAsset.query(source: const LocalSource()),
app.latestRelease.query(