Fix missing profile names and pictures due to stream timing
When a view subscribes to profile updates via streamProfile() or streamProfiles(), the stream now immediately yields any existing profile data from NostrDB before waiting for network updates. Previously, subscribers had to wait up to ~1 second for the subscriptionSwitcherTask to restart the profile listener before receiving any data. During this window, views would display abbreviated pubkeys (e.g., "npub1abc...") or robohash placeholders instead of the cached profile name and picture. The fix adds a simple NDB lookup when creating the stream. This has negligible performance impact since: - It's a one-time operation per subscription (not per update) - The same lookup was already happening in view bodies anyway - NDB lookups are fast local queries A new `yieldCached` parameter (default: true) allows callers to opt out of the initial cached emission. NoteContentView uses this to avoid redundant artifact re-renders — it only needs network updates since its initial render already uses cached profile data. Furthermore, when a profile has no metadata, the display name now shows "npub1yrse...q9ye" instead of "1yrsedhw:8q0pq9ye" for a better UX. Closes: https://github.com/damus-io/damus/issues/3454 Closes: https://github.com/damus-io/damus/issues/3455 Changelog-Changed: Changed abbreviated pubkey format to npub1...xyz for better readability Changelog-Fixed: Fixed instances where a profile would not display profile name and picture for a few seconds Signed-off-by: alltheseas <64376233+alltheseas@users.noreply.github.com> Co-authored-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -1669,6 +1669,7 @@
|
|||||||
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
||||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||||
|
D751FA992EF62C8100E10F1B /* ProfilesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D751FA982EF62C8100E10F1B /* ProfilesManagerTests.swift */; };
|
||||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
||||||
D755B28D2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
D755B28D2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
||||||
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
||||||
@@ -2768,6 +2769,7 @@
|
|||||||
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonCopyableLinkedList.swift; sourceTree = "<group>"; };
|
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonCopyableLinkedList.swift; sourceTree = "<group>"; };
|
||||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||||
|
D751FA982EF62C8100E10F1B /* ProfilesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesManagerTests.swift; sourceTree = "<group>"; };
|
||||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||||
D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP37Draft.swift; sourceTree = "<group>"; };
|
D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP37Draft.swift; sourceTree = "<group>"; };
|
||||||
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
|
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
|
||||||
@@ -5278,6 +5280,7 @@
|
|||||||
D7EBF8BC2E5946F9004EAE29 /* NostrNetworkManagerTests */ = {
|
D7EBF8BC2E5946F9004EAE29 /* NostrNetworkManagerTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D751FA982EF62C8100E10F1B /* ProfilesManagerTests.swift */,
|
||||||
D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */,
|
D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */,
|
||||||
D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */,
|
D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */,
|
||||||
D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */,
|
D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */,
|
||||||
@@ -6252,6 +6255,7 @@
|
|||||||
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */,
|
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */,
|
||||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||||
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
|
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
|
||||||
|
D751FA992EF62C8100E10F1B /* ProfilesManagerTests.swift in Sources */,
|
||||||
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
||||||
D7CBD1D62B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift in Sources */,
|
D7CBD1D62B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift in Sources */,
|
||||||
D7EBF8BB2E59022A004EAE29 /* NostrNetworkManagerTests.swift in Sources */,
|
D7EBF8BB2E59022A004EAE29 /* NostrNetworkManagerTests.swift in Sources */,
|
||||||
|
|||||||
@@ -117,18 +117,49 @@ extension NostrNetworkManager {
|
|||||||
|
|
||||||
// MARK: - Streaming interface
|
// MARK: - Streaming interface
|
||||||
|
|
||||||
func streamProfile(pubkey: Pubkey) -> AsyncStream<ProfileStreamItem> {
|
/// Streams profile updates for a single pubkey.
|
||||||
|
///
|
||||||
|
/// By default, the stream immediately yields the existing profile from NostrDB
|
||||||
|
/// (if available), then continues yielding updates as they arrive from the network.
|
||||||
|
///
|
||||||
|
/// This immediate yield is essential for views that display profile data (names,
|
||||||
|
/// pictures) because the subscription restart has a ~1 second delay. Without it,
|
||||||
|
/// views would flash abbreviated pubkeys or robohash placeholders.
|
||||||
|
///
|
||||||
|
/// Set `yieldCached: false` for subscribers that only need network updates (e.g.,
|
||||||
|
/// re-rendering content when profiles change) and already handle initial state
|
||||||
|
/// through other means.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pubkey: The pubkey to stream profile updates for
|
||||||
|
/// - yieldCached: Whether to immediately yield the cached profile. Defaults to `true`.
|
||||||
|
/// - Returns: An AsyncStream that yields Profile objects
|
||||||
|
func streamProfile(pubkey: Pubkey, yieldCached: Bool = true) -> AsyncStream<ProfileStreamItem> {
|
||||||
return AsyncStream<ProfileStreamItem> { continuation in
|
return AsyncStream<ProfileStreamItem> { continuation in
|
||||||
let stream = ProfileStreamInfo(continuation: continuation)
|
let stream = ProfileStreamInfo(continuation: continuation)
|
||||||
self.add(pubkey: pubkey, stream: stream)
|
self.add(pubkey: pubkey, stream: stream)
|
||||||
|
|
||||||
|
// Yield cached profile immediately so views don't flash placeholder content.
|
||||||
|
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
|
||||||
|
if yieldCached, let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
|
||||||
|
continuation.yield(existingProfile)
|
||||||
|
}
|
||||||
|
|
||||||
continuation.onTermination = { @Sendable _ in
|
continuation.onTermination = { @Sendable _ in
|
||||||
Task { await self.removeStream(pubkey: pubkey, id: stream.id) }
|
Task { await self.removeStream(pubkey: pubkey, id: stream.id) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamProfiles(pubkeys: Set<Pubkey>) -> AsyncStream<ProfileStreamItem> {
|
/// Streams profile updates for multiple pubkeys.
|
||||||
|
///
|
||||||
|
/// Same behavior as `streamProfile(_:yieldCached:)` but for a set of pubkeys.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pubkeys: The set of pubkeys to stream profile updates for
|
||||||
|
/// - yieldCached: Whether to immediately yield cached profiles. Defaults to `true`.
|
||||||
|
/// - Returns: An AsyncStream that yields Profile objects
|
||||||
|
func streamProfiles(pubkeys: Set<Pubkey>, yieldCached: Bool = true) -> AsyncStream<ProfileStreamItem> {
|
||||||
guard !pubkeys.isEmpty else {
|
guard !pubkeys.isEmpty else {
|
||||||
return AsyncStream<ProfileStreamItem> { continuation in
|
return AsyncStream<ProfileStreamItem> { continuation in
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
@@ -141,6 +172,16 @@ extension NostrNetworkManager {
|
|||||||
self.add(pubkey: pubkey, stream: stream)
|
self.add(pubkey: pubkey, stream: stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yield cached profiles immediately so views render correctly from the start.
|
||||||
|
// Callers that only need updates (not initial state) can opt out via yieldCached: false.
|
||||||
|
if yieldCached {
|
||||||
|
for pubkey in pubkeys {
|
||||||
|
if let existingProfile = ndb.lookup_profile_and_copy(pubkey) {
|
||||||
|
continuation.yield(existingProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
continuation.onTermination = { @Sendable _ in
|
continuation.onTermination = { @Sendable _ in
|
||||||
Task {
|
Task {
|
||||||
for pubkey in pubkeys {
|
for pubkey in pubkeys {
|
||||||
|
|||||||
@@ -296,7 +296,9 @@ struct NoteContentView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for await profile in await damus_state.nostrNetwork.profilesManager.streamProfiles(pubkeys: mentionPubkeys) {
|
// Only re-render on network updates, not cached profiles.
|
||||||
|
// Initial render already uses cached profile data via the view hierarchy.
|
||||||
|
for await profile in await damus_state.nostrNetwork.profilesManager.streamProfiles(pubkeys: mentionPubkeys, yieldCached: false) {
|
||||||
await load(force_artifacts: true)
|
await load(force_artifacts: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
|
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
|
||||||
return abbrev_identifier(String(pubkey.npub.dropFirst(4)))
|
let npub = pubkey.npub
|
||||||
|
// Show "npub1abc...xyz" format for better readability
|
||||||
|
return String(npub.prefix(9)) + "..." + String(npub.suffix(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
func abbrev_identifier(_ pubkey: String, amount: Int = 8) -> String {
|
func abbrev_identifier(_ identifier: String, amount: Int = 8) -> String {
|
||||||
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
|
return identifier.prefix(amount) + ":" + identifier.suffix(amount)
|
||||||
}
|
}
|
||||||
|
|||||||
250
damusTests/NostrNetworkManagerTests/ProfilesManagerTests.swift
Normal file
250
damusTests/NostrNetworkManagerTests/ProfilesManagerTests.swift
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
//
|
||||||
|
// ProfilesManagerTests.swift
|
||||||
|
// damusTests
|
||||||
|
//
|
||||||
|
// Created by alltheseas on 2025-12-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import damus
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilesManagerTests: XCTestCase {
|
||||||
|
|
||||||
|
/// Tests that streamProfile with yieldCached: true (default) immediately emits a cached profile.
|
||||||
|
///
|
||||||
|
/// This verifies the fix for missing profile names/pictures: when a view subscribes to
|
||||||
|
/// profile updates, it should immediately receive any cached profile data from NostrDB
|
||||||
|
/// rather than waiting for the network subscription to restart (~1 second delay).
|
||||||
|
func testStreamProfileYieldsCachedProfileByDefault() async throws {
|
||||||
|
let ndb = Ndb.test
|
||||||
|
defer { ndb.close() }
|
||||||
|
|
||||||
|
// Seed a profile into NDB
|
||||||
|
let profilePubkey = test_keypair.pubkey
|
||||||
|
let profileJson = """
|
||||||
|
{"name":"testuser","display_name":"Test User","about":"A test profile","picture":"https://example.com/pic.jpg"}
|
||||||
|
"""
|
||||||
|
let metadataEvent = NostrEvent(
|
||||||
|
content: profileJson,
|
||||||
|
keypair: test_keypair,
|
||||||
|
kind: NostrKind.metadata.rawValue,
|
||||||
|
tags: [],
|
||||||
|
createdAt: UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
)!
|
||||||
|
|
||||||
|
let eventJson = encode_json(metadataEvent)!
|
||||||
|
let relayMessage = "[\"EVENT\",\"subid\",\(eventJson)]"
|
||||||
|
let processed = ndb.processEvent(relayMessage)
|
||||||
|
XCTAssertTrue(processed, "Failed to process metadata event")
|
||||||
|
|
||||||
|
// Give NDB time to process
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
|
// Verify profile is in NDB
|
||||||
|
let cachedProfile = ndb.lookup_profile_and_copy(profilePubkey)
|
||||||
|
XCTAssertNotNil(cachedProfile, "Profile should be cached in NDB")
|
||||||
|
XCTAssertEqual(cachedProfile?.name, "testuser")
|
||||||
|
|
||||||
|
// Create ProfilesManager and test streaming
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: test_keypair)
|
||||||
|
let subscriptionManager = NostrNetworkManager.SubscriptionManager(
|
||||||
|
pool: pool,
|
||||||
|
ndb: ndb,
|
||||||
|
experimentalLocalRelayModelSupport: false
|
||||||
|
)
|
||||||
|
let profilesManager = NostrNetworkManager.ProfilesManager(
|
||||||
|
subscriptionManager: subscriptionManager,
|
||||||
|
ndb: ndb
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test: yieldCached: true (default) should emit immediately
|
||||||
|
let receivedProfile = XCTestExpectation(description: "Should receive cached profile immediately")
|
||||||
|
var emittedProfile: NdbProfile?
|
||||||
|
|
||||||
|
Task {
|
||||||
|
// Default yieldCached: true
|
||||||
|
for await profile in await profilesManager.streamProfile(pubkey: profilePubkey) {
|
||||||
|
emittedProfile = profile
|
||||||
|
receivedProfile.fulfill()
|
||||||
|
break // Only need the first emission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should receive the profile very quickly (not waiting for network)
|
||||||
|
await fulfillment(of: [receivedProfile], timeout: 0.5)
|
||||||
|
|
||||||
|
XCTAssertNotNil(emittedProfile, "Should have received a profile")
|
||||||
|
XCTAssertEqual(emittedProfile?.name, "testuser", "Should receive the cached profile data")
|
||||||
|
XCTAssertEqual(emittedProfile?.display_name, "Test User")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that streamProfile with yieldCached: false does NOT immediately emit cached profiles.
|
||||||
|
///
|
||||||
|
/// This is used by callers like NoteContentView that only need network updates, not
|
||||||
|
/// initial cached state, to avoid redundant artifact re-renders.
|
||||||
|
func testStreamProfileOptOutSkipsCachedProfile() async throws {
|
||||||
|
let ndb = Ndb.test
|
||||||
|
defer { ndb.close() }
|
||||||
|
|
||||||
|
// Seed a profile into NDB
|
||||||
|
let profilePubkey = test_keypair.pubkey
|
||||||
|
let profileJson = """
|
||||||
|
{"name":"testuser","display_name":"Test User","about":"A test profile"}
|
||||||
|
"""
|
||||||
|
let metadataEvent = NostrEvent(
|
||||||
|
content: profileJson,
|
||||||
|
keypair: test_keypair,
|
||||||
|
kind: NostrKind.metadata.rawValue,
|
||||||
|
tags: [],
|
||||||
|
createdAt: UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
)!
|
||||||
|
|
||||||
|
let eventJson = encode_json(metadataEvent)!
|
||||||
|
let relayMessage = "[\"EVENT\",\"subid\",\(eventJson)]"
|
||||||
|
let processed = ndb.processEvent(relayMessage)
|
||||||
|
XCTAssertTrue(processed, "Failed to process metadata event")
|
||||||
|
|
||||||
|
// Give NDB time to process
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
|
// Verify profile is in NDB
|
||||||
|
let cachedProfile = ndb.lookup_profile_and_copy(profilePubkey)
|
||||||
|
XCTAssertNotNil(cachedProfile, "Profile should be cached in NDB")
|
||||||
|
|
||||||
|
// Create ProfilesManager
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: test_keypair)
|
||||||
|
let subscriptionManager = NostrNetworkManager.SubscriptionManager(
|
||||||
|
pool: pool,
|
||||||
|
ndb: ndb,
|
||||||
|
experimentalLocalRelayModelSupport: false
|
||||||
|
)
|
||||||
|
let profilesManager = NostrNetworkManager.ProfilesManager(
|
||||||
|
subscriptionManager: subscriptionManager,
|
||||||
|
ndb: ndb
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test: yieldCached: false should NOT emit immediately
|
||||||
|
let shouldNotReceive = XCTestExpectation(description: "Should NOT receive cached profile")
|
||||||
|
shouldNotReceive.isInverted = true // We expect this to NOT be fulfilled
|
||||||
|
|
||||||
|
Task {
|
||||||
|
// Explicitly opt out of cached emission
|
||||||
|
for await _ in await profilesManager.streamProfile(pubkey: profilePubkey, yieldCached: false) {
|
||||||
|
shouldNotReceive.fulfill() // This should NOT happen
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly - the stream should NOT emit anything
|
||||||
|
await fulfillment(of: [shouldNotReceive], timeout: 0.3)
|
||||||
|
// If we get here without the expectation being fulfilled, the test passes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that streamProfiles with yieldCached: true emits all cached profiles.
|
||||||
|
func testStreamProfilesYieldsCachedProfilesByDefault() async throws {
|
||||||
|
let ndb = Ndb.test
|
||||||
|
defer { ndb.close() }
|
||||||
|
|
||||||
|
// Seed two profiles into NDB
|
||||||
|
let pubkey1 = test_keypair.pubkey
|
||||||
|
let pubkey2 = test_pubkey_2
|
||||||
|
|
||||||
|
let profile1Json = "{\"name\":\"user1\",\"display_name\":\"User One\"}"
|
||||||
|
let profile1Event = NostrEvent(
|
||||||
|
content: profile1Json,
|
||||||
|
keypair: test_keypair,
|
||||||
|
kind: NostrKind.metadata.rawValue,
|
||||||
|
tags: [],
|
||||||
|
createdAt: UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
)!
|
||||||
|
|
||||||
|
// For pubkey2, we need to create an event that appears to come from that pubkey
|
||||||
|
// Since we can't sign for pubkey2, we'll just test with one profile
|
||||||
|
|
||||||
|
let eventJson = encode_json(profile1Event)!
|
||||||
|
let relayMessage = "[\"EVENT\",\"subid\",\(eventJson)]"
|
||||||
|
let processed = ndb.processEvent(relayMessage)
|
||||||
|
XCTAssertTrue(processed, "Failed to process metadata event")
|
||||||
|
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
|
// Create ProfilesManager
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: test_keypair)
|
||||||
|
let subscriptionManager = NostrNetworkManager.SubscriptionManager(
|
||||||
|
pool: pool,
|
||||||
|
ndb: ndb,
|
||||||
|
experimentalLocalRelayModelSupport: false
|
||||||
|
)
|
||||||
|
let profilesManager = NostrNetworkManager.ProfilesManager(
|
||||||
|
subscriptionManager: subscriptionManager,
|
||||||
|
ndb: ndb
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test: yieldCached: true (default) should emit cached profiles
|
||||||
|
let receivedProfile = XCTestExpectation(description: "Should receive cached profile")
|
||||||
|
var emittedProfiles: [NdbProfile] = []
|
||||||
|
|
||||||
|
Task {
|
||||||
|
// Request both pubkeys, but only pubkey1 has a profile in NDB
|
||||||
|
for await profile in await profilesManager.streamProfiles(pubkeys: Set([pubkey1, pubkey2])) {
|
||||||
|
emittedProfiles.append(profile)
|
||||||
|
receivedProfile.fulfill()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fulfillment(of: [receivedProfile], timeout: 0.5)
|
||||||
|
|
||||||
|
XCTAssertEqual(emittedProfiles.count, 1, "Should receive one cached profile")
|
||||||
|
XCTAssertEqual(emittedProfiles.first?.name, "user1")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that streamProfiles with yieldCached: false does NOT emit cached profiles.
|
||||||
|
func testStreamProfilesOptOutSkipsCachedProfiles() async throws {
|
||||||
|
let ndb = Ndb.test
|
||||||
|
defer { ndb.close() }
|
||||||
|
|
||||||
|
// Seed a profile into NDB
|
||||||
|
let profilePubkey = test_keypair.pubkey
|
||||||
|
let profileJson = "{\"name\":\"testuser\"}"
|
||||||
|
let metadataEvent = NostrEvent(
|
||||||
|
content: profileJson,
|
||||||
|
keypair: test_keypair,
|
||||||
|
kind: NostrKind.metadata.rawValue,
|
||||||
|
tags: [],
|
||||||
|
createdAt: UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
)!
|
||||||
|
|
||||||
|
let eventJson = encode_json(metadataEvent)!
|
||||||
|
let relayMessage = "[\"EVENT\",\"subid\",\(eventJson)]"
|
||||||
|
ndb.processEvent(relayMessage)
|
||||||
|
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
|
||||||
|
// Create ProfilesManager
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: test_keypair)
|
||||||
|
let subscriptionManager = NostrNetworkManager.SubscriptionManager(
|
||||||
|
pool: pool,
|
||||||
|
ndb: ndb,
|
||||||
|
experimentalLocalRelayModelSupport: false
|
||||||
|
)
|
||||||
|
let profilesManager = NostrNetworkManager.ProfilesManager(
|
||||||
|
subscriptionManager: subscriptionManager,
|
||||||
|
ndb: ndb
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test: yieldCached: false should NOT emit
|
||||||
|
let shouldNotReceive = XCTestExpectation(description: "Should NOT receive cached profiles")
|
||||||
|
shouldNotReceive.isInverted = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
for await _ in await profilesManager.streamProfiles(pubkeys: Set([profilePubkey]), yieldCached: false) {
|
||||||
|
shouldNotReceive.fulfill()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fulfillment(of: [shouldNotReceive], timeout: 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user