From 93fd67d5882cc3978fd847a78ef47a51a87ce748 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 16 Dec 2025 07:55:36 +0100 Subject: [PATCH] Cached list of owned packages --- app/lib/account/backend.dart | 1 + app/lib/package/backend.dart | 33 ++++++++++++++++++++++++++----- app/lib/shared/redis_cache.dart | 12 +++++++++++ app/test/package/upload_test.dart | 13 ++++++++++-- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/app/lib/account/backend.dart b/app/lib/account/backend.dart index 526e7d6d37..2518546ba7 100644 --- a/app/lib/account/backend.dart +++ b/app/lib/account/backend.dart @@ -662,6 +662,7 @@ Future purgeAccountCache({required String userId}) async { await Future.wait([ cache.userPackageLikes(userId).purgeAndRepeat(), cache.publisherPage(userId).purgeAndRepeat(), + cache.userUploaderOfPackages(userId).purgeAndRepeat(), ]); } diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index 8f28c39f70..7f35e5e7f7 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -250,8 +250,11 @@ class PackageBackend { } /// Streams package names where the [userId] is an uploader. - Stream streamPackagesWhereUserIsUploader(String userId) async* { - var page = await listPackagesForUser(userId); + Stream streamPackagesWhereUserIsUploader( + String userId, { + int pageSize = 100, + }) async* { + var page = await listPackagesForUser(userId, limit: pageSize); while (page.packages.isNotEmpty) { yield* Stream.fromIterable(page.packages); if (page.nextPackage == null) { @@ -262,6 +265,17 @@ class PackageBackend { } } + /// Returns the cached list of package names, where the [userId] is an uploader + /// (package is not under a publisher). + Future> cachedPackagesWhereUserIsUploader(String userId) async { + final list = await cache.userUploaderOfPackages(userId).get(() async { + return await streamPackagesWhereUserIsUploader( + userId, + ).take(1000).toList(); + }); + return list as List; + } + /// Returns the latest releases info of a package. Future latestReleases(Package package) async { // TODO: implement runtimeVersion-specific release calculation @@ -1522,6 +1536,11 @@ class PackageBackend { asyncQueue.addAsyncFn( () => _postUploadTasks(package, newVersion, outgoingEmail), ); + if (isNew && agent is AuthenticatedUser) { + asyncQueue.addAsyncFn( + () => cache.userUploaderOfPackages(agent.userId).purge(), + ); + } _logger.info('Post-upload tasks completed in ${sw.elapsed}.'); return (pv, uploadMessages); @@ -1865,7 +1884,7 @@ class PackageBackend { User uploader, { required String consentRequestFromAgent, }) async { - await withRetryTransaction(db, (tx) async { + final uploaderUserId = await withRetryTransaction(db, (tx) async { final packageKey = db.emptyKey.append(Package, id: packageName); final package = (await tx.lookup([packageKey])).first as Package; @@ -1878,7 +1897,7 @@ class PackageBackend { } if (package.containsUploader(uploader.userId)) { // The requested uploaderEmail is already part of the uploaders. - return; + return uploader.userId; } // Add [uploaderEmail] to uploaders and commit. @@ -1892,7 +1911,9 @@ class PackageBackend { package: packageName, ), ); + return uploader.userId; }); + await purgeAccountCache(userId: uploaderUserId); triggerPackagePostUpdates( packageName, skipReanalysis: true, @@ -1923,7 +1944,7 @@ class PackageBackend { uploaderEmail = uploaderEmail.toLowerCase(); final authenticatedUser = await requireAuthenticatedWebUser(); final user = authenticatedUser.user; - await withRetryTransaction(db, (tx) async { + final uploaderUserId = await withRetryTransaction(db, (tx) async { final packageKey = db.emptyKey.append(Package, id: packageName); final package = await tx.lookupOrNull(packageKey); if (package == null) { @@ -1976,7 +1997,9 @@ class PackageBackend { uploaderUser: uploader, ), ); + return uploader.userId; }); + await purgeAccountCache(userId: uploaderUserId); triggerPackagePostUpdates( packageName, skipReanalysis: true, diff --git a/app/lib/shared/redis_cache.dart b/app/lib/shared/redis_cache.dart index d537dd10d4..0422bb0a27 100644 --- a/app/lib/shared/redis_cache.dart +++ b/app/lib/shared/redis_cache.dart @@ -298,6 +298,18 @@ class CachePatterns { ), )[userId]; + Entry> userUploaderOfPackages(String userId) => _cache + .withPrefix('user-uploader-of-packages/') + .withTTL(Duration(minutes: 60)) + .withCodec(utf8) + .withCodec(json) + .withCodec( + wrapAsCodec( + encode: (List l) => l, + decode: (d) => (d as List).cast(), + ), + )[userId]; + Entry secretValue(String secretId) => _cache .withPrefix('secret-value/') .withTTL(Duration(minutes: 60)) diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index a6eaed348b..37e997976b 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -110,6 +110,12 @@ void main() { testWithProfile( 'successful new package', fn: () async { + final user = await accountBackend.lookupUserByEmail('user@pub.dev'); + expect( + await packageBackend.cachedPackagesWhereUserIsUploader(user.userId), + isEmpty, + ); + final dateBeforeTest = clock.now().toUtc(); final pubspecContent = generatePubspecYaml('new_package', '1.2.3'); final message = await createPubApiClient(authToken: userClientToken) @@ -121,8 +127,6 @@ void main() { expect(message.success.message, contains('1.2.3')); // verify state - final user = await accountBackend.lookupUserByEmail('user@pub.dev'); - final pkgKey = dbService.emptyKey.append(Package, id: 'new_package'); final package = (await dbService.lookup([pkgKey])).single!; expect(package.name, 'new_package'); @@ -143,6 +147,11 @@ void main() { expect(pv.publisherId, isNull); await asyncQueue.ongoingProcessing; + expect( + await packageBackend.cachedPackagesWhereUserIsUploader(user.userId), + ['new_package'], + ); + expect(fakeEmailSender.sentMessages, hasLength(1)); final email = fakeEmailSender.sentMessages.single; expect(email.recipients.single.email, user.email);