mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Move deletions to foreground
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user