diff --git a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift index 287693a2..6bc1a17f 100644 --- a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift @@ -35,9 +35,9 @@ class NostrNetworkManager { let reader: SubscriptionManager let profilesManager: ProfilesManager - init(delegate: Delegate) { + init(delegate: Delegate, addNdbToRelayPool: Bool = true) { self.delegate = delegate - let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair) + let pool = RelayPool(ndb: addNdbToRelayPool ? delegate.ndb : nil, keypair: delegate.keypair) self.pool = pool let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb, experimentalLocalRelayModelSupport: self.delegate.experimentalLocalRelayModelSupport) let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader) diff --git a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift index 70c190a3..8dd9ecd8 100644 --- a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift @@ -25,8 +25,6 @@ extension NostrNetworkManager { category: "subscription_manager" ) - let EXTRA_VERBOSE_LOGGING: Bool = false - init(pool: RelayPool, ndb: Ndb, experimentalLocalRelayModelSupport: Bool) { self.pool = pool self.ndb = ndb @@ -127,7 +125,7 @@ extension NostrNetworkManager { // In normal mode: Issuing EOSE requires EOSE from both NDB and the network, since they are all considered separate relays // In experimental local relay model mode: Issuing EOSE requires only EOSE from NDB, since that is the only relay that "matters" let canIssueEOSE = switch streamMode { - case .ndbFirst: (ndbEOSEIssued) + case .ndbFirst, .ndbOnly: (ndbEOSEIssued) case .ndbAndNetworkParallel: (ndbEOSEIssued && (networkEOSEIssued || !connectedToNetwork)) } @@ -142,6 +140,7 @@ extension NostrNetworkManager { var latestNoteTimestampSeen: UInt32? = nil let startNetworkStreamTask = { + guard streamMode.shouldStreamFromNetwork else { return } networkStreamTask = Task { while !Task.isCancelled { let optimizedFilters = filters.map { @@ -171,7 +170,7 @@ extension NostrNetworkManager { } } - if streamMode.optimizeNetworkFilter == false { + if streamMode.optimizeNetworkFilter == false && streamMode.shouldStreamFromNetwork { // Start streaming from the network straight away startNetworkStreamTask() } @@ -199,7 +198,7 @@ extension NostrNetworkManager { logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)") continuation.yield(item) ndbEOSEIssued = true - if streamMode.optimizeNetworkFilter { + if streamMode.optimizeNetworkFilter && streamMode.shouldStreamFromNetwork { startNetworkStreamTask() } yieldEOSEIfReady() @@ -237,11 +236,8 @@ extension NostrNetworkManager { logStreamPipelineStats("RelayPool_Handler_\(id)", "SubscriptionManager_Network_Stream_\(id)") switch item { case .event(let event): - if EXTRA_VERBOSE_LOGGING { - Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Received kind \(event.kind, privacy: .public) event with id \(event.id.hex(), privacy: .private) from the network") - } switch streamMode { - case .ndbFirst: + case .ndbFirst, .ndbOnly: break // NO-OP case .ndbAndNetworkParallel: continuation.yield(.event(lender: NdbNoteLender(ownedNdbNote: event))) @@ -534,6 +530,8 @@ extension NostrNetworkManager { /// Returns notes from both NostrDB and the network, in parallel, treating it with similar importance against the network relays. Generic EOSE is fired when EOSE is received from both the network and NostrDB /// `optimizeNetworkFilter`: Returns notes from ndb, then streams from the network with an added "since" filter set to the latest note stored on ndb. case ndbAndNetworkParallel(optimizeNetworkFilter: Bool) + /// Ignores the network. Used for testing purposes + case ndbOnly var optimizeNetworkFilter: Bool { switch self { @@ -541,6 +539,19 @@ extension NostrNetworkManager { return optimizeNetworkFilter case .ndbAndNetworkParallel(optimizeNetworkFilter: let optimizeNetworkFilter): return optimizeNetworkFilter + case .ndbOnly: + return false + } + } + + var shouldStreamFromNetwork: Bool { + switch self { + case .ndbFirst: + return true + case .ndbAndNetworkParallel: + return true + case .ndbOnly: + return false } } } diff --git a/damus/Core/Nostr/RelayPool.swift b/damus/Core/Nostr/RelayPool.swift index be40b3ed..e44e7ba5 100644 --- a/damus/Core/Nostr/RelayPool.swift +++ b/damus/Core/Nostr/RelayPool.swift @@ -35,7 +35,7 @@ actor RelayPool { var request_queue: [QueuedRequest] = [] var seen: [NoteId: Set] = [:] var counts: [RelayURL: UInt64] = [:] - var ndb: Ndb + var ndb: Ndb? /// The keypair used to authenticate with relays var keypair: Keypair? var message_received_function: (((String, RelayDescriptor)) -> Void)? @@ -71,7 +71,7 @@ actor RelayPool { seen.removeAll() } - init(ndb: Ndb, keypair: Keypair? = nil) { + init(ndb: Ndb?, keypair: Keypair? = nil) { self.ndb = ndb self.keypair = keypair @@ -179,7 +179,7 @@ actor RelayPool { case .string(let str) = msg else { return } - let _ = self.ndb.processEvent(str, originRelayURL: relay_id) + let _ = self.ndb?.processEvent(str, originRelayURL: relay_id) self.message_received_function?((str, desc)) }) let relay = Relay(descriptor: desc, connection: conn) @@ -400,10 +400,10 @@ actor RelayPool { switch req { case .typical(let r): if case .event = r, let rstr = make_nostr_req(r) { - let _ = ndb.process_client_event(rstr) + let _ = ndb?.process_client_event(rstr) } case .custom(let string): - let _ = ndb.process_client_event(string) + let _ = ndb?.process_client_event(string) } } diff --git a/damus/Core/Storage/DamusState.swift b/damus/Core/Storage/DamusState.swift index 85d7a052..170277a6 100644 --- a/damus/Core/Storage/DamusState.swift +++ b/damus/Core/Storage/DamusState.swift @@ -39,7 +39,7 @@ class DamusState: HeadlessDamusState, ObservableObject { let favicon_cache: FaviconCache private(set) var nostrNetwork: NostrNetworkManager - init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) { + init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache, addNdbToRelayPool: Bool = true) { self.keypair = keypair self.likes = likes self.boosts = boosts @@ -72,7 +72,7 @@ class DamusState: HeadlessDamusState, ObservableObject { self.favicon_cache = FaviconCache() let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters) - let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate) + let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate, addNdbToRelayPool: addNdbToRelayPool) self.nostrNetwork = nostrNetwork self.wallet.nostrNetwork = nostrNetwork } diff --git a/damusTests/AuthIntegrationTests.swift b/damusTests/AuthIntegrationTests.swift index dc250802..a86b2221 100644 --- a/damusTests/AuthIntegrationTests.swift +++ b/damusTests/AuthIntegrationTests.swift @@ -103,13 +103,16 @@ final class AuthIntegrationTests: XCTestCase { try! await pool.add_relay(relay_descriptor) XCTAssertEqual(pool.relays.count, 1) let connection_expectation = XCTestExpectation(description: "Waiting for connection") + await pool.connect() Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in - if pool.num_connected == 1 { - connection_expectation.fulfill() - timer.invalidate() + Task { + if await pool.num_connected == 1 { + connection_expectation.fulfill() + timer.invalidate() + } } } - wait(for: [connection_expectation], timeout: 30.0) + await fulfillment(of: [connection_expectation], timeout: 30.0) XCTAssertEqual(pool.num_connected, 1) // Assert that no AUTH messages have been received XCTAssertEqual(received_messages.count, 0) @@ -148,13 +151,16 @@ final class AuthIntegrationTests: XCTestCase { try! await pool.add_relay(relay_descriptor) XCTAssertEqual(pool.relays.count, 1) let connection_expectation = XCTestExpectation(description: "Waiting for connection") + await pool.connect() Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in - if pool.num_connected == 1 { - connection_expectation.fulfill() - timer.invalidate() + Task { + if pool.num_connected == 1 { + connection_expectation.fulfill() + timer.invalidate() + } } } - wait(for: [connection_expectation], timeout: 30.0) + await fulfillment(of: [connection_expectation], timeout: 30.0) XCTAssertEqual(pool.num_connected, 1) // Assert that no AUTH messages have been received XCTAssertEqual(received_messages.count, 0) @@ -173,13 +179,15 @@ final class AuthIntegrationTests: XCTestCase { timer.invalidate() } } - wait(for: [msg_expectation], timeout: 30.0) + await fulfillment(of: [msg_expectation], timeout: 30.0) // Assert that AUTH message has been received XCTAssertTrue(received_messages.count >= 1, "expected recieved_messages to be >= 1") + if received_messages.count < 1 { return } // End test early let json_received = try! JSONSerialization.jsonObject(with: received_messages[0].data(using: .utf8)!, options: []) as! [Any] XCTAssertEqual(json_received[0] as! String, "AUTH") // Assert that we've replied with the AUTH response XCTAssertEqual(sent_messages.count, 2) + if sent_messages.count < 2 { return } let json_sent = try! JSONSerialization.jsonObject(with: sent_messages[1].data(using: .utf8)!, options: []) as! [Any] XCTAssertEqual(json_sent[0] as! String, "AUTH") let sent_msg = json_sent[1] as! [String: Any] diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift index 0be2400b..bf471dfc 100644 --- a/damusTests/Mocking/MockDamusState.swift +++ b/damusTests/Mocking/MockDamusState.swift @@ -12,12 +12,12 @@ import EmojiPicker // Generates a test damus state with configurable mock parameters func generate_test_damus_state( mock_profile_info: [Pubkey: Profile]?, - home: HomeModel? = nil + home: HomeModel? = nil, + addNdbToRelayPool: Bool = true ) -> DamusState { // Create a unique temporary directory let ndb = Ndb.test let our_pubkey = test_pubkey - let pool = RelayPool(ndb: ndb) let settings = UserSettingsStore() let profiles: Profiles = { @@ -51,7 +51,8 @@ func generate_test_damus_state( ndb: ndb, quote_reposts: .init(our_pubkey: our_pubkey), emoji_provider: DefaultEmojiProvider(showAllVariations: false), - favicon_cache: .init() + favicon_cache: .init(), + addNdbToRelayPool: addNdbToRelayPool ) home?.damus_state = damus diff --git a/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift b/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift index 01ce5e56..e6593fd4 100644 --- a/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift +++ b/damusTests/NostrNetworkManagerTests/NostrNetworkManagerTests.swift @@ -14,7 +14,10 @@ class NostrNetworkManagerTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. - damusState = generate_test_damus_state(mock_profile_info: nil) + damusState = generate_test_damus_state( + mock_profile_info: nil, + addNdbToRelayPool: false // Don't give RelayPool any access to Ndb. This will prevent incoming notes from affecting our test + ) let notesJSONL = getTestNotesJSONL() @@ -37,13 +40,12 @@ class NostrNetworkManagerTests: XCTestCase { return try! String(contentsOf: fileURL, encoding: .utf8) } - func ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter, expectedCount: Int) { + func ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter, expectedCount: Int) async { let endOfStream = XCTestExpectation(description: "Stream should receive EOSE") - let gotAtLeastExpectedCount = XCTestExpectation(description: "Stream should receive at least the expected number of items") var receivedCount = 0 var eventIds: Set = [] Task { - for await item in self.damusState!.nostrNetwork.reader.advancedStream(filters: [filter], streamMode: .ndbFirst) { + for await item in self.damusState!.nostrNetwork.reader.advancedStream(filters: [filter], streamMode: .ndbOnly) { switch item { case .event(let lender): try? lender.borrow { event in @@ -53,9 +55,6 @@ class NostrNetworkManagerTests: XCTestCase { } eventIds.insert(event.id) } - if receivedCount == expectedCount { - gotAtLeastExpectedCount.fulfill() - } case .eose: continue case .ndbEose: @@ -67,7 +66,7 @@ class NostrNetworkManagerTests: XCTestCase { } } } - wait(for: [endOfStream, gotAtLeastExpectedCount], timeout: 10.0) + await fulfillment(of: [endOfStream], timeout: 15.0) XCTAssertEqual(receivedCount, expectedCount, "Event IDs: \(eventIds.map({ $0.hex() }))") } @@ -83,14 +82,11 @@ class NostrNetworkManagerTests: XCTestCase { /// nak req --kind 1 ws://localhost:10547 | wc -l /// ``` func testNdbSubscription() async { - try! await damusState?.nostrNetwork.userRelayList.set(userRelayList: NIP65.RelayList()) - await damusState?.nostrNetwork.connect() - - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text]), expectedCount: 57) - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(authors: [Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!]), expectedCount: 22) - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.boost], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!]), expectedCount: 5) - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text, .boost, .zap], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!], limit: 500), expectedCount: 5) - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], limit: 10), expectedCount: 10) - ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], until: UInt32(Date.now.timeIntervalSince1970), limit: 10), expectedCount: 10) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text]), expectedCount: 57) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(authors: [Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!]), expectedCount: 22) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.boost], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!]), expectedCount: 5) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text, .boost, .zap], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!], limit: 500), expectedCount: 5) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], limit: 10), expectedCount: 10) + await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], until: UInt32(Date.now.timeIntervalSince1970), limit: 10), expectedCount: 10) } } diff --git a/justfile b/justfile index 60ca6e3e..a98fe0c0 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ build: - xcodebuild -scheme damus -sdk iphoneos -destination 'platform=iOS Simulator,OS=18.2,name=iPhone 16' -quiet | xcbeautify --quieter + xcodebuild -scheme damus -sdk iphoneos -destination 'platform=iOS Simulator,OS=26.0,name=iPhone 16e' -quiet | xcbeautify --quieter test: - xcodebuild test -scheme damus -destination 'platform=iOS Simulator,OS=18.2,name=iPhone 16' -quiet | xcbeautify --quieter + xcodebuild test -scheme damus -destination 'platform=iOS Simulator,OS=26.0,name=iPhone 16e' | xcbeautify