NostrDao is what every event passes through, inbound and outbound, so its two decisions carry everything downstream: which of two copies of an event wins, and what survives when the enrichment after a write fails. Both were described in comments and neither was asserted. Deduplication, at all four positions. A first sighting is stored. A strictly newer copy replaces the stored one. An older copy is ignored. And -- the case that actually distinguishes the implementations -- a redelivery carrying the *same* timestamp is a no-op, because the comparison is a strict `>`. That last one is not hypothetical: relays redeliver and negentropy re-syncs, so the common case is the same event arriving again unchanged, and a `>=` there would rewrite the row on every delivery. The publish durability split, which is where a bug shipped. `commitPublishedNostrEvent` is the durable half -- mark the unsigned row signed, store the event, queue a broadcast per relay -- and indexing is best-effort enrichment that runs in its own transaction. They used to share one, so any throw in indexing rolled back `signedAt` too. Because the notary drains one unsigned row at a time, that row was then re-selected forever and every event queued behind it went unsigned, including the MLS key package that is enqueued last. The test provokes the failure the way the code itself would fail: publishing with no target relays reaches `relayURLs.first()` inside the try and throws. It then asserts `signedAt` and the stored event both survived. The happy path is covered alongside it, asserting a broadcast request per target relay, so the durability test cannot pass by publishing nothing at all. Also covered: an event from an author with no profile leaves a "LOADING..." placeholder stamped GENESIS_AT rather than nothing, since that row is the only record that the pubkey was seen and needs fetching; and rescheduleBroadcastNostrEventRequests re-queueing a broadcast and re-linking it to the chat line when the event is a group message that has one, without inventing a relation when it does not. One test began as a wrong assumption and the schema corrected it. The "no chat line" case was first written against an event id that had never been stored, and failed with SQLite 787: BroadcastNostrEventRequest.nostrEventId is a foreign key onto NostrEvent. So the real invariant is that a broadcast cannot be scheduled for an event the caller has not saved; the test now stores the event and leaves only the chat line missing, and says so in a comment rather than quietly seeding around it. Verified by mutation: relaxing the dedup comparison to `>=` fails the same-timestamp test; removing the try/catch around indexing so the throw propagates fails the durability test. Both mutations were reverted; no production source is touched by this commit. Uses `runBlocking<Unit>` on the durability test because its last expression is an assertNotNull, and a test method that returns a value is rejected by the JUnit4 runner outright. 9 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…