The first live subscription: kind 1059 p-tagged to us, on every DM relay, open until the wallet changes or the process ends. That covers direct messages and the Marmot Welcome events that make us a member of a group — so a DM now lands whether or not anyone has the chat list open, and a group invite is noticed without waiting for the next screen to schedule a sync. Nothing is removed from the queue yet. ChatRoomListViewModel and ChatMessageListViewModel still schedule what they always did; this runs beside them and the duplicate events cost a no-op storeNostrEvent each. The screens stop doing it in the last step, once groups are covered too. **No `since`.** NIP-59 randomizes a wrap's created_at into the past, and our own outbound path stamps them with TimeUtils.randomWithTwoDays() — so a wrap published right now can carry a timestamp two days old, and a `since` anywhere near the present would silently drop it. "Some messages just never arrive" is the worst failure mode to debug, and re-receiving a wrap costs one no-op write. **`limit` instead.** NIP-01 scopes limit to the initial query — the stored events a relay sends before EOSE — and explicitly not to the stream that follows, so limit=100 bounds what a reconnect costs without touching the live tail. Filling in the rest of the history stays negentropy's job. **What ends a subscription, and what doesn't.** CLOSED does, and is retried with backoff from 5s to 5m, jumping straight to the maximum when the reason parses as back-pressure — answering "too many subscriptions" by promptly opening another is how one refusal becomes a flood. EOSE does not end anything: it is the boundary between stored history and the live tail, and treating it as an end is precisely what made every sync a poll. It does reset the backoff, since a subscription that got that far was working and whatever ended it is a new problem. A dropped socket needs nothing from this loop at all — the pool replays the REQ on reconnect and the collector, attached to the socket client rather than to a session, simply starts receiving again. A NOTICE is logged and otherwise ignored. It carries no subscription id, so it reaches every collector on the socket; acting on one here would let an unrelated relay complaint tear this subscription down. Events are saved inline rather than in a launched coroutine, so they land in the order the relay sent them and the socket's buffer does the back-pressure, instead of fanning a burst out into a coroutine per event. Wiring: LiveSubscriptionManager is built by SynchronizationViewModel because it has to share that class's RelaysSocketManager — a second one would mean a second RelayPool and a duplicate socket per relay — and is launched inside the same per-wallet supervisorScope as the three pumps, so a wallet switch tears it down. It deliberately does not take from the pumps' Semaphore(4): a subscription that never finishes would hold a permit forever and permanently halve backfill throughput. The comment there now says so. isBackPressure moves out of SynchronizationViewModel to network/relays/RelayBackPressure.kt, unchanged, now that two callers need it. 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…