Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,10 @@
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D75154BF2EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */; };
D75154C02EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */; };
D75154C12EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */; };
D75154C22EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */; };
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
D755B28D2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
Expand Down Expand Up @@ -1709,6 +1713,7 @@
D78F08182D7F7F7500FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
D78F081A2D7F803100FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
D795356B2EBD28A800AACF98 /* AppLifecycleHandlingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795356A2EBD289D00AACF98 /* AppLifecycleHandlingTests.swift */; };
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
Expand Down Expand Up @@ -2766,6 +2771,7 @@
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>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbUseLock.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>"; };
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
Expand All @@ -2786,6 +2792,7 @@
D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; };
D795356A2EBD289D00AACF98 /* AppLifecycleHandlingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLifecycleHandlingTests.swift; sourceTree = "<group>"; };
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3297,6 +3304,7 @@
4C9054862A6AEB4500811EEC /* nostrdb */ = {
isa = PBXGroup;
children = (
D75154BE2EC5910600BF2CB2 /* NdbUseLock.swift */,
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */,
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
D7F5630F2DEE71BB008509DE /* NdbFilter.swift */,
Expand Down Expand Up @@ -5275,6 +5283,7 @@
D7EBF8BC2E5946F9004EAE29 /* NostrNetworkManagerTests */ = {
isa = PBXGroup;
children = (
D795356A2EBD289D00AACF98 /* AppLifecycleHandlingTests.swift */,
D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */,
D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */,
D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */,
Expand Down Expand Up @@ -5737,6 +5746,7 @@
4CC6AA792CAB688500989CEF /* sha256.c in Sources */,
4CC6AA7B2CAB688500989CEF /* likely.c in Sources */,
4CC6AA7F2CAB688500989CEF /* htable.c in Sources */,
D75154C02EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */,
4CC6AA862CAB688500989CEF /* list.c in Sources */,
4CC6AA872CAB688500989CEF /* utf8.c in Sources */,
4CC6AA892CAB688500989CEF /* debug.c in Sources */,
Expand Down Expand Up @@ -6259,6 +6269,7 @@
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */,
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D795356B2EBD28A800AACF98 /* AppLifecycleHandlingTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
Expand Down Expand Up @@ -6777,6 +6788,7 @@
82D6FC3A2CD99F7900C925F4 /* WideEventView.swift in Sources */,
82D6FC3B2CD99F7900C925F4 /* LongformView.swift in Sources */,
82D6FC3C2CD99F7900C925F4 /* LongformPreview.swift in Sources */,
D75154C22EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */,
82D6FC3D2CD99F7900C925F4 /* EventShell.swift in Sources */,
82D6FC3E2CD99F7900C925F4 /* MentionView.swift in Sources */,
82D6FC3F2CD99F7900C925F4 /* EventLoaderView.swift in Sources */,
Expand Down Expand Up @@ -7202,6 +7214,7 @@
D73E5F302C6A97F4007EB227 /* EventProfile.swift in Sources */,
D73E5F312C6A97F4007EB227 /* EventMenu.swift in Sources */,
D73E5F322C6A97F4007EB227 /* EventMutingContainerView.swift in Sources */,
D75154C12EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */,
D73E5F332C6A97F4007EB227 /* ZapEvent.swift in Sources */,
5C8F97362EB46145009399B1 /* LiveStreamView.swift in Sources */,
D73E5F342C6A97F4007EB227 /* TextEvent.swift in Sources */,
Expand Down Expand Up @@ -7466,6 +7479,7 @@
4CC6AAC52CAB688500989CEF /* likely.c in Sources */,
4CC6AAC92CAB688500989CEF /* htable.c in Sources */,
4CC6AAD02CAB688500989CEF /* list.c in Sources */,
D75154BF2EC5910A00BF2CB2 /* NdbUseLock.swift in Sources */,
4CC6AAD12CAB688500989CEF /* utf8.c in Sources */,
4CC6AAD32CAB688500989CEF /* debug.c in Sources */,
4CC6AAD42CAB688500989CEF /* str.c in Sources */,
Expand Down
9 changes: 2 additions & 7 deletions damus/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,7 @@ struct ContentView: View {
Log.debug("App background signal handling: App being backgrounded", for: .app_lifecycle)
let startTime = CFAbsoluteTimeGetCurrent()
await damus_state.nostrNetwork.handleAppBackgroundRequest() // Close ndb streaming tasks before closing ndb to avoid memory errors
Log.debug("App background signal handling: Nostr network closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime)
damus_state.ndb.close()
Log.debug("App background signal handling: Ndb closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime)
Log.debug("App background signal handling: Nostr network and Ndb closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime)
this_app.endBackgroundTask(bgTask)
}
break
Expand All @@ -552,9 +550,7 @@ struct ContentView: View {
Task {
await damusClosingTask?.value // Wait for the closing task to finish before reopening things, to avoid race conditions
damusClosingTask = nil
damus_state.ndb.reopen()
// Pinging the network will automatically reconnect any dead websocket connections
await damus_state.nostrNetwork.ping()
await damus_state.nostrNetwork.handleAppForegroundRequest()
}
@unknown default:
break
Expand Down Expand Up @@ -1144,7 +1140,6 @@ extension LossyLocalNotification {
}
}


func logout(_ state: DamusState?)
{
state?.close()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,19 @@ class NostrNetworkManager {
await self.pool.disconnect()
}

func handleAppBackgroundRequest() async {
func handleAppBackgroundRequest(beforeClosingNdb operationBeforeClosingNdb: (() async -> Void)? = nil) async {
// Mark NDB as closed without actually closing it, to avoid new tasks from using NostrDB
// self.delegate.ndb.markClosed()
await self.reader.cancelAllTasks()
await self.pool.cleanQueuedRequestForSessionEnd()
await operationBeforeClosingNdb?()
self.delegate.ndb.close()
}

func handleAppForegroundRequest() async {
self.delegate.ndb.reopen()
// Pinging the network will automatically reconnect any dead websocket connections
await self.ping()
}

func close() async {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// AppLifecycleHandlingTests.swift
// damus
//
// Created by Daniel D’Aquino on 2025-11-06.
//

import XCTest
@testable import damus


class AppLifecycleHandlingTests: XCTestCase {

func getTestNotesJSONL() -> String {
// Get the path for the test_notes.jsonl file in the same folder as this test file
let testBundle = Bundle(for: type(of: self))
let fileURL = testBundle.url(forResource: "test_notes", withExtension: "jsonl")!

// Load the contents of the file
return try! String(contentsOf: fileURL, encoding: .utf8)
}

/// Tests for some race conditions between the app closing down and streams opening throughout the app
/// See https://github.com/damus-io/damus/issues/3245 for more context.
///
/// **Note:** Time delays are intentionally added because we actually want to provoke possible race conditions,
/// so using proper waiting mechanisms would defeat the purpose of the test.
func testAppLifecycleRaceConditions() async throws {
let damusState = generate_test_damus_state(mock_profile_info: nil)

let notesJSONL = getTestNotesJSONL()
for noteText in notesJSONL.split(separator: "\n") {
let _ = damusState.ndb.processEvent("[\"EVENT\",\"subid\",\(String(noteText))]")
}

// Give some time ndb some time to fill up
try? await Task.sleep(for: .milliseconds(2000))



// Start measuring the time elapsed for debugging
let startTime = CFAbsoluteTimeGetCurrent()
func getElapsedTimeMiliseconds() -> String {
return "\((CFAbsoluteTimeGetCurrent() - startTime) * 1000) ms"
}


Task.detached {
for i in 0...10000 {
try await Task.sleep(for: .milliseconds(Int.random(in: 0...10)))
print("APP_LIFECYCLE_TEST \(i): About to close Ndb. Elapsed time: \(getElapsedTimeMiliseconds())")
damusState.ndb.close()
print("APP_LIFECYCLE_TEST \(i): Closed Ndb. Elapsed time: \(getElapsedTimeMiliseconds())")
print("APP_LIFECYCLE_TEST \(i): Reopening Ndb. Elapsed time: \(getElapsedTimeMiliseconds())")
_ = damusState.ndb.reopen()
print("APP_LIFECYCLE_TEST \(i): Reopened Ndb. Elapsed time: \(getElapsedTimeMiliseconds())")

}
}
for i in 0...10000 {
do {
try await Task.sleep(for: .milliseconds(Int.random(in: 0...10)))
print("APP_LIFECYCLE_TEST \(i): Starting new query. Elapsed time: \(getElapsedTimeMiliseconds())")
guard let txn = NdbTxn(ndb: damusState.ndb) else { continue }
_ = try damusState.ndb.query(with: txn, filters: [try NdbFilter(from: NostrFilter(kinds: [.text], limit: 1000))], maxResults: 500)
}
catch {
print("APP_LIFECYCLE_TEST \(i): Query error: \(error). Elapsed time: \(getElapsedTimeMiliseconds())")
}
print("APP_LIFECYCLE_TEST \(i): Finished query. Elapsed time: \(getElapsedTimeMiliseconds())")
}
}
}
4 changes: 2 additions & 2 deletions nostrdb/Ndb+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ extension Ndb {
/// - maxSimultaneousResults: Maximum number of initial results to return
/// - Returns: AsyncStream of StreamItem events
/// - Throws: NdbStreamError if subscription fails
func subscribe(filters: [NostrFilter], maxSimultaneousResults: Int = 1000) throws(NdbStreamError) -> AsyncStream<StreamItem> {
func subscribe(filters: [NostrFilter], maxSimultaneousResults: Int = 1000) throws -> AsyncStream<StreamItem> {
let ndbFilters: [NdbFilter]
do {
ndbFilters = try filters.toNdbFilters()
} catch {
throw .cannotConvertFilter(error)
throw NdbStreamError.cannotConvertFilter(error)
}
return try self.subscribe(filters: ndbFilters, maxSimultaneousResults: maxSimultaneousResults)
}
Expand Down
Loading