Add EntityPreloader for batched profile metadata preloading

Implements an actor-based preloading system to efficiently fetch profile
metadata for note authors and referenced users. The EntityPreloader queues
requests and batches them intelligently (500 pubkeys or 1 second timeout)
to avoid network overload while improving UX by ensuring profiles are
available when rendering notes.

Key changes:
- Add EntityPreloader actor with queue-based batching logic
- Integrate with SubscriptionManager via PreloadStrategy enum
- Add lifecycle management (start/stop on app foreground/background)
- Skip preload for pubkeys already cached in ndb
- Include comprehensive test suite with 11 test cases covering batching,
  deduplication, and edge cases
- Optimize ProfilePicView to load from ndb before first render

Closes: https://github.com/damus-io/damus/issues/gh-3511
Changelog-Added: Profile metadata preloading for improved timeline performance
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2026-01-22 20:47:31 -08:00
parent 4eac3c576f
commit 438d537ff6
10 changed files with 1168 additions and 11 deletions

View File

@@ -1700,6 +1700,10 @@
D77135D62E7B78D700E7639F /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77135D22E7B766300E7639F /* DataExtensions.swift */; };
D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
D776BE402F232B17002DA1C9 /* EntityPreloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D776BE3F2F232B17002DA1C9 /* EntityPreloader.swift */; };
D776BE412F232B17002DA1C9 /* EntityPreloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D776BE3F2F232B17002DA1C9 /* EntityPreloader.swift */; };
D776BE422F232B17002DA1C9 /* EntityPreloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D776BE3F2F232B17002DA1C9 /* EntityPreloader.swift */; };
D776BE442F23301A002DA1C9 /* EntityPreloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D776BE432F233012002DA1C9 /* EntityPreloaderTests.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
D77DA2C42F19CA48000B7093 /* AsyncStreamUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DA2C32F19CA40000B7093 /* AsyncStreamUtilities.swift */; };
D77DA2C52F19CA48000B7093 /* AsyncStreamUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DA2C32F19CA40000B7093 /* AsyncStreamUtilities.swift */; };
@@ -2820,6 +2824,8 @@
D76BE18B2E0CF3D5004AD0C6 /* Interests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interests.swift; sourceTree = "<group>"; };
D77135D22E7B766300E7639F /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = "<group>"; };
D773BC5E2C6D538500349F0A /* CommentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentItem.swift; sourceTree = "<group>"; };
D776BE3F2F232B17002DA1C9 /* EntityPreloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityPreloader.swift; sourceTree = "<group>"; };
D776BE432F233012002DA1C9 /* EntityPreloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityPreloaderTests.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
D77DA2C32F19CA40000B7093 /* AsyncStreamUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStreamUtilities.swift; sourceTree = "<group>"; };
D77DA2C72F19D452000B7093 /* NegentropyUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NegentropyUtilities.swift; sourceTree = "<group>"; };
@@ -3867,6 +3873,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
D776BE432F233012002DA1C9 /* EntityPreloaderTests.swift */,
D77DA2CD2F1C2596000B7093 /* SubscriptionManagerNegentropyTests.swift */,
D74723ED2F15B0D6002DA12A /* NegentropySupportTests.swift */,
D72734292F089EE600F90677 /* NdbMigrationTests.swift */,
@@ -5238,6 +5245,7 @@
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
isa = PBXGroup;
children = (
D776BE3F2F232B17002DA1C9 /* EntityPreloader.swift */,
D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */,
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
@@ -5851,6 +5859,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */,
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */,
D776BE402F232B17002DA1C9 /* EntityPreloader.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
@@ -6376,6 +6385,7 @@
E06336AA2B75832100A88E6B /* ImageMetadataTest.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */,
D776BE442F23301A002DA1C9 /* EntityPreloaderTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
D7100CB62EEA3E20008D94B7 /* AutoSaveViewModelTests.swift in Sources */,
@@ -6474,6 +6484,7 @@
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
82D6FAE32CD99F7900C925F4 /* FollowedNotify.swift in Sources */,
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
D776BE412F232B17002DA1C9 /* EntityPreloader.swift in Sources */,
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
5C4FA8012DC5119300CE658C /* FollowPackPreview.swift in Sources */,
@@ -6964,6 +6975,7 @@
4C36247A2D5EA20C00DD066E /* bech32.c in Sources */,
4C3624792D5EA20200DD066E /* bolt11.c in Sources */,
4C3624782D5EA1FE00DD066E /* error.c in Sources */,
D776BE422F232B17002DA1C9 /* EntityPreloader.swift in Sources */,
4C3624772D5EA1FA00DD066E /* nostr_bech32.c in Sources */,
4C3624762D5EA1F600DD066E /* content_parser.c in Sources */,
4C3624752D5EA1E000DD066E /* block.c in Sources */,