diff --git a/README.md b/README.md index 6b091c0..aa6ae44 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It combines app discovery, publisher identity, direct APK distribution, and social trust signals into a different model for Android app distribution. -This repo is the [Kotlin](https://kotlinlang.org) / [Jetpack Compose](https://developer.android.com/compose) client for [Zapstore](https://zapstore.dev). Catalog listings, curated stacks, releases, and publisher profiles sync from Nostr relays through the local `purplequartz` module. +This repo is the [Kotlin](https://kotlinlang.org) / [Jetpack Compose](https://developer.android.com/compose) client for [Zapstore](https://zapstore.dev). Catalog listings, curated stacks, releases, and publisher profiles sync from Nostr relays through the local `iolite` module. ## Get the app @@ -43,7 +43,7 @@ Or call Gradle directly: ./gradlew test ``` -`./gradlew test` runs unit tests for the app and `purplequartz`. Instrumented UI tests need a connected device or emulator: +`./gradlew test` runs unit tests for the app and `iolite`. Instrumented UI tests need a connected device or emulator: ```bash ./gradlew connectedDebugAndroidTest @@ -52,7 +52,7 @@ Or call Gradle directly: ## Layout - `src/main/kotlin/dev/zapstore/app/` — Compose UI, navigation, view models, and catalog wiring -- `purplequartz/` — local-first Nostr client (SQLite event store, relay sessions, outbox routing) on top of [Quartz](https://github.com/vitorpamplona/amethyst) +- `iolite/` — local-first Nostr client (SQLite event store, relay sessions, outbox routing) on top of [Quartz](https://github.com/vitorpamplona/amethyst) ## Contributing diff --git a/build.gradle.kts b/build.gradle.kts index 167408d..0bda23d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "dev.zapstore.app" - compileSdk = providers.gradleProperty("purplequartz.compileSdk").orElse("37").get().toInt() + compileSdk = providers.gradleProperty("iolite.compileSdk").orElse("37").get().toInt() defaultConfig { applicationId = "dev.zapstore.beta" @@ -28,7 +28,7 @@ kotlin { } dependencies { - implementation(project(":purplequartz")) + implementation(project(":iolite")) implementation(platform(libs.compose.bom)) implementation(libs.activity.compose) implementation(libs.compose.foundation) diff --git a/purplequartz/build.gradle.kts b/iolite/build.gradle.kts similarity index 83% rename from purplequartz/build.gradle.kts rename to iolite/build.gradle.kts index b413c13..41fcec0 100644 --- a/purplequartz/build.gradle.kts +++ b/iolite/build.gradle.kts @@ -3,8 +3,8 @@ plugins { } android { - namespace = "dev.zapstore.purplequartz" - compileSdk = providers.gradleProperty("purplequartz.compileSdk").orElse("37").get().toInt() + namespace = "dev.zapstore.iolite" + compileSdk = providers.gradleProperty("iolite.compileSdk").orElse("37").get().toInt() defaultConfig { minSdk = 29 diff --git a/iolite/consumer-rules.pro b/iolite/consumer-rules.pro new file mode 100644 index 0000000..6cf31aa --- /dev/null +++ b/iolite/consumer-rules.pro @@ -0,0 +1 @@ +# Iolite requires no consumer-specific shrinking rules. diff --git a/purplequartz/gradle.lockfile b/iolite/gradle.lockfile similarity index 100% rename from purplequartz/gradle.lockfile rename to iolite/gradle.lockfile diff --git a/purplequartz/settings-gradle.lockfile b/iolite/settings-gradle.lockfile similarity index 100% rename from purplequartz/settings-gradle.lockfile rename to iolite/settings-gradle.lockfile diff --git a/purplequartz/src/androidTest/kotlin/dev/zapstore/purplequartz/QuartzCompatibilityInstrumentedTest.kt b/iolite/src/androidTest/kotlin/dev/zapstore/iolite/QuartzCompatibilityInstrumentedTest.kt similarity index 94% rename from purplequartz/src/androidTest/kotlin/dev/zapstore/purplequartz/QuartzCompatibilityInstrumentedTest.kt rename to iolite/src/androidTest/kotlin/dev/zapstore/iolite/QuartzCompatibilityInstrumentedTest.kt index 2984d92..2ddfc37 100644 --- a/purplequartz/src/androidTest/kotlin/dev/zapstore/purplequartz/QuartzCompatibilityInstrumentedTest.kt +++ b/iolite/src/androidTest/kotlin/dev/zapstore/iolite/QuartzCompatibilityInstrumentedTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress @@ -102,7 +102,7 @@ class QuartzCompatibilityInstrumentedTest { @Test fun facadeRecreationReadsPersistedRowsWithoutNetwork() = runBlocking { val context = InstrumentationRegistry.getInstrumentation().targetContext - val databaseName = "purplequartz-offline-${UUID.randomUUID()}.db" + val databaseName = "iolite-offline-${UUID.randomUUID()}.db" val path = context.getDatabasePath(databaseName).absolutePath val event = knownValidEvent() context.deleteDatabase(databaseName) @@ -118,11 +118,11 @@ class QuartzCompatibilityInstrumentedTest { try { repeat(2) { val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - val purpleQuartz = PurpleQuartz.create( + val purpleQuartz = Iolite.create( context = context, websocketBuilder = BasicOkHttpWebSocket.Builder { okHttpClient }, parentScope = scope, - config = PurpleQuartzConfig(databaseName = databaseName), + config = IoliteConfig(databaseName = databaseName), ) try { val state = purpleQuartz.query(Filter(ids = listOf(event.id))).first() @@ -143,15 +143,15 @@ class QuartzCompatibilityInstrumentedTest { @Test fun api29ConsumerConstructsQueriesAllSourcesAndCloses() = runBlocking { val context = InstrumentationRegistry.getInstrumentation().targetContext - val databaseName = "purplequartz-consumer-${UUID.randomUUID()}.db" + val databaseName = "iolite-consumer-${UUID.randomUUID()}.db" val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val okHttpClient = OkHttpClient() val relay = "ws://127.0.0.1:1".normalizeRelayUrl() - val purpleQuartz = PurpleQuartz.create( + val purpleQuartz = Iolite.create( context = context, websocketBuilder = BasicOkHttpWebSocket.Builder { okHttpClient }, parentScope = scope, - config = PurpleQuartzConfig(databaseName = databaseName), + config = IoliteConfig(databaseName = databaseName), ) try { diff --git a/purplequartz/src/main/AndroidManifest.xml b/iolite/src/main/AndroidManifest.xml similarity index 100% rename from purplequartz/src/main/AndroidManifest.xml rename to iolite/src/main/AndroidManifest.xml diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartz.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/Iolite.kt similarity index 98% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartz.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/Iolite.kt index b896b06..c3118cb 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartz.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/Iolite.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import android.content.Context import android.net.ConnectivityManager @@ -51,13 +51,13 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong import kotlin.time.Duration -class PurpleQuartz private constructor( +class Iolite private constructor( private val store: ObservableEventStore, private val client: INostrClient, private val scope: CoroutineScope, private val job: Job, private val databasePath: String, - private val config: PurpleQuartzConfig, + private val config: IoliteConfig, private val eventVerifier: (Event) -> Boolean, private val connectivityManager: ConnectivityManager?, private val refreshCache: QueryRefreshCache, @@ -126,7 +126,7 @@ class PurpleQuartz private constructor( fun query(filters: List, options: QueryOptions = QueryOptions.local()): Flow = callbackFlow { if (closed.get()) { - trySend(QueryState(emptyList(), QueryPhase.Failed, error = QueryError.Lifecycle("PurpleQuartz is closed"))) + trySend(QueryState(emptyList(), QueryPhase.Failed, error = QueryError.Lifecycle("Iolite is closed"))) close() return@callbackFlow } @@ -140,7 +140,7 @@ class PurpleQuartz private constructor( val session = QuerySession(snapshot, options, this) synchronized(sessionsLock) { if (closed.get()) { - trySend(QueryState(emptyList(), QueryPhase.Failed, error = QueryError.Lifecycle("PurpleQuartz is closed"))) + trySend(QueryState(emptyList(), QueryPhase.Failed, error = QueryError.Lifecycle("Iolite is closed"))) close() return@callbackFlow } @@ -253,7 +253,7 @@ class PurpleQuartz private constructor( } private suspend fun withStoreOperation(block: suspend () -> T): T { - check(!closed.get()) { "PurpleQuartz is closing" } + check(!closed.get()) { "Iolite is closing" } return block() } @@ -902,7 +902,7 @@ class PurpleQuartz private constructor( cleanup() val terminal = lastState.copy( phase = QueryPhase.Failed, - error = QueryError.Lifecycle("PurpleQuartz is closed"), + error = QueryError.Lifecycle("Iolite is closed"), ) lastState = terminal producer.trySend(terminal) @@ -936,8 +936,8 @@ class PurpleQuartz private constructor( context: Context, websocketBuilder: WebsocketBuilder, parentScope: CoroutineScope, - config: PurpleQuartzConfig = PurpleQuartzConfig(), - ): PurpleQuartz { + config: IoliteConfig = IoliteConfig(), + ): Iolite { config.validate() val parentJob = requireActiveParentJob(parentScope) val appContext = context.applicationContext @@ -945,7 +945,7 @@ class PurpleQuartz private constructor( val databaseExisted = databaseFile.exists() val path = databaseFile.canonicalPath synchronized(openDatabases) { - check(path !in openDatabases) { "A PurpleQuartz instance already owns this database" } + check(path !in openDatabases) { "A Iolite instance already owns this database" } openDatabases += path } val job = SupervisorJob(parentJob) @@ -960,7 +960,7 @@ class PurpleQuartz private constructor( databaseExisted = databaseExisted, ) client = NostrClient(websocketBuilder, scope) - return PurpleQuartz( + return Iolite( store, client, scope, @@ -990,17 +990,17 @@ class PurpleQuartz private constructor( eventStore: IEventStore, client: INostrClient, parentScope: CoroutineScope, - config: PurpleQuartzConfig = PurpleQuartzConfig(), + config: IoliteConfig = IoliteConfig(), databasePath: String = "test-${System.nanoTime()}", eventVerifier: (Event) -> Boolean = DEFAULT_EVENT_VERIFIER, refreshCache: QueryRefreshCache = InMemoryQueryRefreshCache(), clock: EpochMillisClock = SYSTEM_CLOCK, dispatcher: CoroutineDispatcher = Dispatchers.IO, - ): PurpleQuartz { + ): Iolite { config.validate() val job = SupervisorJob(requireActiveParentJob(parentScope)) val scope = CoroutineScope(parentScope.coroutineContext + job + dispatcher) - return PurpleQuartz( + return Iolite( store = ObservableEventStore(eventStore), client = client, scope = scope, diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartzConfig.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/IoliteConfig.kt similarity index 94% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartzConfig.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/IoliteConfig.kt index b850b67..0d1d908 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/PurpleQuartzConfig.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/IoliteConfig.kt @@ -1,12 +1,12 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -data class PurpleQuartzConfig( - val databaseName: String = "purplequartz.db", +data class IoliteConfig( + val databaseName: String = "iolite.db", val oneShotTimeout: Duration = 30.seconds, val ingestionCapacity: Int = 1_024, val expirationSweepInterval: Duration = 1.minutes, diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/OutboxRouter.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/OutboxRouter.kt similarity index 97% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/OutboxRouter.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/OutboxRouter.kt index eabb4af..a22d278 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/OutboxRouter.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/OutboxRouter.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -15,7 +15,7 @@ import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds /** - * NIP-65 outbox routing on top of [PurpleQuartz]. + * NIP-65 outbox routing on top of [Iolite]. * * Resolution is local-first: kind-10002 relay lists are read from the local * store and only fetched from [bootstrapRelays] when the TTL'd freshness cache @@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds * store holds. */ class OutboxRouter( - private val client: PurpleQuartz, + private val client: Iolite, private val bootstrapRelays: Set, private val cacheDuration: Duration = 6.hours, private val resolveTimeout: Duration = 10.seconds, diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryFingerprint.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryFingerprint.kt similarity index 99% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryFingerprint.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/QueryFingerprint.kt index 4e55b13..0814dca 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryFingerprint.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryFingerprint.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryRefreshCache.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryRefreshCache.kt similarity index 97% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryRefreshCache.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/QueryRefreshCache.kt index 0cebfe2..2b7324f 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryRefreshCache.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryRefreshCache.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import android.content.Context import android.content.SharedPreferences @@ -96,7 +96,7 @@ internal class SharedPreferencesQueryRefreshCache private constructor( } companion object { - private const val PREFERENCES_NAME = "dev.zapstore.purplequartz.query-refresh-v1" + private const val PREFERENCES_NAME = "dev.zapstore.iolite.query-refresh-v1" private const val MAX_ENTRIES = 1_024 fun create( diff --git a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryTypes.kt b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryTypes.kt similarity index 99% rename from purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryTypes.kt rename to iolite/src/main/kotlin/dev/zapstore/iolite/QueryTypes.kt index b98c703..444cd40 100644 --- a/purplequartz/src/main/kotlin/dev/zapstore/purplequartz/QueryTypes.kt +++ b/iolite/src/main/kotlin/dev/zapstore/iolite/QueryTypes.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/LegacyQueryTestAdapter.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/LegacyQueryTestAdapter.kt similarity index 87% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/LegacyQueryTestAdapter.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/LegacyQueryTestAdapter.kt index dd3229a..ba7a0ce 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/LegacyQueryTestAdapter.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/LegacyQueryTestAdapter.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -46,8 +46,8 @@ internal object QuerySync { internal val QueryState.sync: QueryPhase get() = phase -internal fun PurpleQuartz.query(filter: Filter, source: QuerySource): Flow = +internal fun Iolite.query(filter: Filter, source: QuerySource): Flow = query(filter, source.options) -internal fun PurpleQuartz.query(filters: List, source: QuerySource): Flow = +internal fun Iolite.query(filters: List, source: QuerySource): Flow = query(filters, source.options) diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/ObservationTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/ObservationTest.kt similarity index 94% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/ObservationTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/ObservationTest.kt index 754899f..baf0224 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/ObservationTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/ObservationTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient @@ -30,7 +30,7 @@ class ObservationTest { @Test fun `sessions do not re-emit on other sessions inserts`() = runBlocking { val client = MultiSubClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( RecordingEventStore(), client, this, @@ -64,7 +64,7 @@ class ObservationTest { @Test fun `replaceable update re-emits with the new version`() = runBlocking { val client = MultiSubClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( RecordingEventStore(), client, this, @@ -96,11 +96,11 @@ class ObservationTest { fun `events are persisted via a single batch at eose`() = runBlocking { val client = MultiSubClient() val store = RecordingEventStore() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( store, client, this, - config = PurpleQuartzConfig(ingestFlushInterval = 10.seconds), + config = IoliteConfig(ingestFlushInterval = 10.seconds), eventVerifier = { true }, ) val states = CopyOnWriteArrayList() @@ -134,11 +134,11 @@ class ObservationTest { @Test fun `remote burst is coalesced and complete state contains every event`() = runBlocking { val client = MultiSubClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( RecordingEventStore(), client, this, - config = PurpleQuartzConfig(ingestBatchSize = 1_000, ingestFlushInterval = 10.seconds), + config = IoliteConfig(ingestBatchSize = 1_000, ingestFlushInterval = 10.seconds), eventVerifier = { true }, ) val states = CopyOnWriteArrayList() @@ -169,11 +169,11 @@ class ObservationTest { val store = RecordingEventStore(rejectBatchInserts = true) val events = (0 until 100).map { signedEvent(content = "duplicate-$it") } events.forEach { store.insert(it) } - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( store, client, this, - config = PurpleQuartzConfig(ingestBatchSize = 100, ingestFlushInterval = 10.seconds), + config = IoliteConfig(ingestBatchSize = 100, ingestFlushInterval = 10.seconds), eventVerifier = { true }, ) val states = CopyOnWriteArrayList() @@ -199,11 +199,11 @@ class ObservationTest { @Test fun `slow local collector cannot block remote persistence`() = runBlocking { val client = MultiSubClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( RecordingEventStore(), client, this, - config = PurpleQuartzConfig(ingestBatchSize = 500, ingestFlushInterval = 10.seconds), + config = IoliteConfig(ingestBatchSize = 500, ingestFlushInterval = 10.seconds), eventVerifier = { true }, ) val slowCollection = launch { @@ -234,7 +234,7 @@ class ObservationTest { } private fun kotlinx.coroutines.CoroutineScope.collect( - purpleQuartz: PurpleQuartz, + purpleQuartz: Iolite, filter: Filter, states: MutableList, ): Job = launch { diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/OutboxRouterTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/OutboxRouterTest.kt similarity index 95% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/OutboxRouterTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/OutboxRouterTest.kt index 536aa59..d3970c2 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/OutboxRouterTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/OutboxRouterTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient @@ -37,7 +37,7 @@ class OutboxRouterTest { arrayOf("r", "http://insecure.example"), ))) val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val router = OutboxRouter(purpleQuartz, bootstrap) var resolved: Set? = null @@ -60,7 +60,7 @@ class OutboxRouterTest { val filter = Filter(authors = listOf(author), kinds = listOf(OutboxRouter.RELAY_LIST_KIND), limit = 1) refreshCache.recordRefresh(QueryFingerprint.create(listOf(filter), bootstrap), System.currentTimeMillis()) val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, refreshCache = refreshCache) + val purpleQuartz = Iolite.createForTesting(store, client, this, refreshCache = refreshCache) val router = OutboxRouter(purpleQuartz, bootstrap) val resolved = withTimeout(2.seconds) { router.resolveReadRelays(author) } @@ -75,7 +75,7 @@ class OutboxRouterTest { val store = RouterTestStore() store.insert(relayListEvent(arrayOf(arrayOf("r", outbox.url)))) val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val router = OutboxRouter(purpleQuartz, bootstrap) val collection = launch { @@ -100,7 +100,7 @@ class OutboxRouterTest { val store = RouterTestStore() store.insert(relayListEvent(arrayOf(arrayOf("r", outbox.url)))) val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val router = OutboxRouter(purpleQuartz, bootstrap) val collection = launch { @@ -125,7 +125,7 @@ class OutboxRouterTest { fun `queryWithOutbox keeps the fallback relays when nothing resolves`() = runBlocking { val store = RouterTestStore() val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val router = OutboxRouter(purpleQuartz, bootstrap) val collection = launch { @@ -149,7 +149,7 @@ class OutboxRouterTest { fun `resolveReadRelays returns empty when the fetch cannot complete`() = runBlocking { val store = RouterTestStore() val client = RouterTestClient() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val router = OutboxRouter(purpleQuartz, bootstrap, resolveTimeout = 1.seconds) var resolved: Set? = null diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/PublicContractsTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/PublicContractsTest.kt similarity index 84% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/PublicContractsTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/PublicContractsTest.kt index 0f42c49..dfb095a 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/PublicContractsTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/PublicContractsTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -21,14 +21,14 @@ import org.junit.Test class PublicContractsTest { @Test fun `config rejects invalid lifecycle values`() { - assertFails { PurpleQuartzConfig(databaseName = "../store.db").validate() } - assertFails { PurpleQuartzConfig(oneShotTimeout = 0.milliseconds).validate() } - assertFails { PurpleQuartzConfig(ingestionCapacity = 0).validate() } - assertFails { PurpleQuartzConfig(expirationSweepInterval = 0.milliseconds).validate() } - assertFails { PurpleQuartzConfig(ingestBatchSize = 0).validate() } - assertFails { PurpleQuartzConfig(ingestFlushInterval = 0.milliseconds).validate() } - assertFails { PurpleQuartzConfig(pruneRules = mapOf(9735 to 0.milliseconds)).validate() } - assertFails { PurpleQuartzConfig(pruneRules = mapOf(-1 to 1.hours)).validate() } + assertFails { IoliteConfig(databaseName = "../store.db").validate() } + assertFails { IoliteConfig(oneShotTimeout = 0.milliseconds).validate() } + assertFails { IoliteConfig(ingestionCapacity = 0).validate() } + assertFails { IoliteConfig(expirationSweepInterval = 0.milliseconds).validate() } + assertFails { IoliteConfig(ingestBatchSize = 0).validate() } + assertFails { IoliteConfig(ingestFlushInterval = 0.milliseconds).validate() } + assertFails { IoliteConfig(pruneRules = mapOf(9735 to 0.milliseconds)).validate() } + assertFails { IoliteConfig(pruneRules = mapOf(-1 to 1.hours)).validate() } } @Test @@ -60,7 +60,7 @@ class PublicContractsTest { @Test fun `published Quartz APIs used by the facade remain callable`() { // This is deliberately a compile-level compatibility gate for the public APIs - // PurpleQuartz relies on. Behavioral compatibility is covered by Android tests. + // Iolite relies on. Behavioral compatibility is covered by Android tests. val invalidEvent = Event( id = "0".repeat(64), pubKey = "0".repeat(64), diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryInvariantTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryInvariantTest.kt similarity index 94% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryInvariantTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/QueryInvariantTest.kt index ba2ebfa..0544ecd 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryInvariantTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryInvariantTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient @@ -45,7 +45,7 @@ class QueryInvariantTest { cachedFor = 6.hours, ) val firstClient = ControlledClient() - val first = PurpleQuartz.createForTesting( + val first = Iolite.createForTesting( ControlledEventStore(), firstClient, this, @@ -65,7 +65,7 @@ class QueryInvariantTest { first.close() val secondClient = ControlledClient() - val second = PurpleQuartz.createForTesting( + val second = Iolite.createForTesting( ControlledEventStore(), secondClient, this, @@ -98,7 +98,7 @@ class QueryInvariantTest { val event = signedEvent(content = "persisted") store.insert(event) val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( store, client, this, @@ -127,7 +127,7 @@ class QueryInvariantTest { ) cache.recordRefresh(QueryFingerprint.create(listOf(filter), setOf(relay)), clock.now()) val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -160,7 +160,7 @@ class QueryInvariantTest { val client = ControlledClient() val store = ControlledEventStore() store.insert(signedEvent(content = "cached")) - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( store, client, this, @@ -200,11 +200,11 @@ class QueryInvariantTest { val timeoutCache = InMemoryQueryRefreshCache() val timeoutClient = ControlledClient() - val timeoutQuartz = PurpleQuartz.createForTesting( + val timeoutQuartz = Iolite.createForTesting( ControlledEventStore(), timeoutClient, this, - config = PurpleQuartzConfig(oneShotTimeout = 50.milliseconds), + config = IoliteConfig(oneShotTimeout = 50.milliseconds), eventVerifier = { true }, refreshCache = timeoutCache, ) @@ -218,7 +218,7 @@ class QueryInvariantTest { val failureCache = InMemoryQueryRefreshCache() val failureClient = ControlledClient() - val failureQuartz = PurpleQuartz.createForTesting( + val failureQuartz = Iolite.createForTesting( ControlledEventStore(failInserts = true), failureClient, this, @@ -247,7 +247,7 @@ class QueryInvariantTest { cachedFor = 1.hours, ) val fingerprint = QueryFingerprint.create(listOf(filter), setOf(relay)) - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(failQueriesAfter = 1), client, this, @@ -271,7 +271,7 @@ class QueryInvariantTest { @Test fun `reconnect advances generation and accepts replacement EOSE`() = runBlocking { val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -304,7 +304,7 @@ class QueryInvariantTest { @Test fun `local and remote stream preserves items through disconnect and reconnect`() = runBlocking { val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -346,7 +346,7 @@ class QueryInvariantTest { fun `facade close emits lifecycle failure and completes active collectors`() = runBlocking { val client = ControlledClient() val store = ControlledEventStore() - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, eventVerifier = { true }) + val purpleQuartz = Iolite.createForTesting(store, client, this, eventVerifier = { true }) val states = CopyOnWriteArrayList() val collection = collect(purpleQuartz, QuerySource.Remote(setOf(relay)), states) @@ -368,7 +368,7 @@ class QueryInvariantTest { fun `local first completion waits for commit and final projection`() = runBlocking { val client = ControlledClient() val store = ControlledEventStore(blockInserts = true) - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, eventVerifier = { true }) + val purpleQuartz = Iolite.createForTesting(store, client, this, eventVerifier = { true }) val states = CopyOnWriteArrayList() val source = QuerySource.LocalAndRemote(setOf(relay), RemoteMode.OneShot(5.seconds)) val collection = collect(purpleQuartz, source, states) @@ -394,8 +394,8 @@ class QueryInvariantTest { fun `timeout unsubscribes immediately then drains accepted persistence`() = runBlocking { val client = ControlledClient() val store = ControlledEventStore(blockInserts = true) - val config = PurpleQuartzConfig(oneShotTimeout = 100.milliseconds) - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, config, eventVerifier = { true }) + val config = IoliteConfig(oneShotTimeout = 100.milliseconds) + val purpleQuartz = Iolite.createForTesting(store, client, this, config, eventVerifier = { true }) val states = CopyOnWriteArrayList() val source = QuerySource.Remote(setOf(relay), RemoteMode.OneShot()) val collection = collect(purpleQuartz, source, states) @@ -426,8 +426,8 @@ class QueryInvariantTest { fun `bounded ingestion fails closed instead of dropping EOSE`() = runBlocking { val client = ControlledClient() val store = ControlledEventStore(blockInserts = true) - val config = PurpleQuartzConfig(ingestionCapacity = 1, ingestBatchSize = 1) - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, config, eventVerifier = { true }) + val config = IoliteConfig(ingestionCapacity = 1, ingestBatchSize = 1) + val purpleQuartz = Iolite.createForTesting(store, client, this, config, eventVerifier = { true }) val states = CopyOnWriteArrayList() val source = QuerySource.Remote(setOf(relay), RemoteMode.OneShot(5.seconds)) val collection = collect(purpleQuartz, source, states) @@ -451,7 +451,7 @@ class QueryInvariantTest { fun `remote persistence failure keeps directly emitted item visible`() = runBlocking { val client = ControlledClient() val store = ControlledEventStore(failInserts = true) - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this, eventVerifier = { true }) + val purpleQuartz = Iolite.createForTesting(store, client, this, eventVerifier = { true }) val states = CopyOnWriteArrayList() val collection = collect(purpleQuartz, QuerySource.Remote(setOf(relay)), states) val event = signedEvent(content = "visible-before-save") @@ -472,11 +472,11 @@ class QueryInvariantTest { val store = ControlledEventStore() val nowMillis = 1_800_000_000_000L val clock = MutableEpochClock(nowMillis) - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( store, ControlledClient(), this, - config = PurpleQuartzConfig( + config = IoliteConfig( expirationSweepInterval = 50.milliseconds, pruneRules = mapOf(9735 to 1.hours), ), @@ -503,7 +503,7 @@ class QueryInvariantTest { @Test fun `relay CLOSED exposes closed state and fails the query`() = runBlocking { val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -527,7 +527,7 @@ class QueryInvariantTest { fun `one closed relay does not terminate a healthy multi-relay stream`() = runBlocking { val otherRelay = "wss://other.example".normalizeRelayUrl() val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -558,7 +558,7 @@ class QueryInvariantTest { @Test fun `subscription startup failure is classified as incompatible dependency`() = runBlocking { val client = ControlledClient(failSubscribe = true) - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -576,7 +576,7 @@ class QueryInvariantTest { @Test fun `stream query emits every ingested event at eose`() = runBlocking { val client = ControlledClient() - val purpleQuartz = PurpleQuartz.createForTesting( + val purpleQuartz = Iolite.createForTesting( ControlledEventStore(), client, this, @@ -604,7 +604,7 @@ class QueryInvariantTest { } private fun kotlinx.coroutines.CoroutineScope.collect( - purpleQuartz: PurpleQuartz, + purpleQuartz: Iolite, source: QuerySource, states: MutableList, ): Job = launch { diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryRefreshCacheTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryRefreshCacheTest.kt similarity index 98% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryRefreshCacheTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/QueryRefreshCacheTest.kt index 1cc90ed..7e9c41a 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryRefreshCacheTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryRefreshCacheTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl diff --git a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryWiringTest.kt b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryWiringTest.kt similarity index 93% rename from purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryWiringTest.kt rename to iolite/src/test/kotlin/dev/zapstore/iolite/QueryWiringTest.kt index dac5b57..98175c0 100644 --- a/purplequartz/src/test/kotlin/dev/zapstore/purplequartz/QueryWiringTest.kt +++ b/iolite/src/test/kotlin/dev/zapstore/iolite/QueryWiringTest.kt @@ -1,4 +1,4 @@ -package dev.zapstore.purplequartz +package dev.zapstore.iolite import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient @@ -26,7 +26,7 @@ class QueryWiringTest { @Test fun `local query emits an empty local seed without subscribing`() = runBlocking { val client = RecordingClient() - val purpleQuartz = PurpleQuartz.createForTesting(MemoryEventStore(), client, this) + val purpleQuartz = Iolite.createForTesting(MemoryEventStore(), client, this) val states = purpleQuartz.query(Filter(kinds = listOf(1))).take(1).toList() @@ -39,7 +39,7 @@ class QueryWiringTest { @Test fun `remote request uses exactly the supplied relay set`() = runBlocking { val client = RecordingClient() - val purpleQuartz = PurpleQuartz.createForTesting(MemoryEventStore(), client, this) + val purpleQuartz = Iolite.createForTesting(MemoryEventStore(), client, this) val relays = setOf( "wss://one.example".normalizeRelayUrl(), "wss://two.example".normalizeRelayUrl(), @@ -66,7 +66,7 @@ class QueryWiringTest { @Test fun `foreground refresh bypasses relay retry delays`() = runBlocking { val client = RecordingClient() - val purpleQuartz = PurpleQuartz.createForTesting(MemoryEventStore(), client, this) + val purpleQuartz = Iolite.createForTesting(MemoryEventStore(), client, this) purpleQuartz.refreshConnections() @@ -77,7 +77,7 @@ class QueryWiringTest { @Test fun `relay traffic toggle disconnects and reconnects the client`() = runBlocking { val client = RecordingClient() - val purpleQuartz = PurpleQuartz.createForTesting(MemoryEventStore(), client, this) + val purpleQuartz = Iolite.createForTesting(MemoryEventStore(), client, this) purpleQuartz.setRelayTrafficEnabled(false) purpleQuartz.setRelayTrafficEnabled(true) @@ -93,7 +93,7 @@ class QueryWiringTest { val store = MemoryEventStore() val event = testEvent("offline") store.insert(event) - val purpleQuartz = PurpleQuartz.createForTesting(store, client, this) + val purpleQuartz = Iolite.createForTesting(store, client, this) val relay = "wss://offline.example".normalizeRelayUrl() purpleQuartz.setRelayTrafficEnabled(false) val states = CopyOnWriteArrayList() diff --git a/purplequartz/consumer-rules.pro b/purplequartz/consumer-rules.pro deleted file mode 100644 index 8cb36d5..0000000 --- a/purplequartz/consumer-rules.pro +++ /dev/null @@ -1 +0,0 @@ -# PurpleQuartz requires no consumer-specific shrinking rules. diff --git a/settings.gradle.kts b/settings.gradle.kts index b9150b6..2bf644c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,4 @@ dependencyResolutionManagement { } rootProject.name = "zapstore" -include(":purplequartz") +include(":iolite") diff --git a/src/main/kotlin/dev/zapstore/app/Catalog.kt b/src/main/kotlin/dev/zapstore/app/Catalog.kt index 19312fd..811a5cd 100644 --- a/src/main/kotlin/dev/zapstore/app/Catalog.kt +++ b/src/main/kotlin/dev/zapstore/app/Catalog.kt @@ -5,14 +5,14 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket -import dev.zapstore.purplequartz.OutboxRouter -import dev.zapstore.purplequartz.PurpleQuartz -import dev.zapstore.purplequartz.PurpleQuartzConfig -import dev.zapstore.purplequartz.QueryOptions -import dev.zapstore.purplequartz.QueryPhase -import dev.zapstore.purplequartz.QueryState -import dev.zapstore.purplequartz.RemoteMode -import dev.zapstore.purplequartz.SourceMode +import dev.zapstore.iolite.OutboxRouter +import dev.zapstore.iolite.Iolite +import dev.zapstore.iolite.IoliteConfig +import dev.zapstore.iolite.QueryOptions +import dev.zapstore.iolite.QueryPhase +import dev.zapstore.iolite.QueryState +import dev.zapstore.iolite.RemoteMode +import dev.zapstore.iolite.SourceMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -163,14 +163,14 @@ interface CatalogRepository { fun setRelayTrafficEnabled(enabled: Boolean) = Unit } -class PurpleQuartzCatalogRepository(context: Context) : CatalogRepository { +class IoliteCatalogRepository(context: Context) : CatalogRepository { private val applicationContext = context.applicationContext private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val profileFlows = ConcurrentHashMap>() private val appAuthorFlows = ConcurrentHashMap>() private val c1AuthorFlows = ConcurrentHashMap>() private val relaySigner = MutableStateFlow(null) - private var client: PurpleQuartz? = null + private var client: Iolite? = null private var outboxRouter: OutboxRouter? = null @Volatile private var relayTrafficEnabled = true @@ -181,12 +181,12 @@ class PurpleQuartzCatalogRepository(context: Context) : CatalogRepository { } } - private fun client(): PurpleQuartz = - client ?: PurpleQuartz.create( + private fun client(): Iolite = + client ?: Iolite.create( applicationContext, BasicOkHttpWebSocket.Builder { OkHttpClient() }, scope, - config = PurpleQuartzConfig( + config = IoliteConfig( // Zap receipts are the unbounded firehose. Addressable catalog kinds are // bounded by supersession; assets back the release feed and stay. pruneRules = mapOf(Catalog.zapReceiptKind to 90.days), diff --git a/src/main/kotlin/dev/zapstore/app/ViewModels.kt b/src/main/kotlin/dev/zapstore/app/ViewModels.kt index 8d6b461..77a389e 100644 --- a/src/main/kotlin/dev/zapstore/app/ViewModels.kt +++ b/src/main/kotlin/dev/zapstore/app/ViewModels.kt @@ -9,8 +9,8 @@ import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewModelScope import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import dev.zapstore.purplequartz.QueryPhase -import dev.zapstore.purplequartz.RemoteMode +import dev.zapstore.iolite.QueryPhase +import dev.zapstore.iolite.RemoteMode import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt b/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt index 1d12129..08a33ac 100644 --- a/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt +++ b/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.ProcessLifecycleOwner class ZapstoreApplication : Application() { val catalogRepository: CatalogRepository by lazy { - PurpleQuartzCatalogRepository(this) + IoliteCatalogRepository(this) } override fun onCreate() { diff --git a/src/test/kotlin/dev/zapstore/app/ViewModelsTest.kt b/src/test/kotlin/dev/zapstore/app/ViewModelsTest.kt index cfe183a..7a80f3c 100644 --- a/src/test/kotlin/dev/zapstore/app/ViewModelsTest.kt +++ b/src/test/kotlin/dev/zapstore/app/ViewModelsTest.kt @@ -3,11 +3,11 @@ package dev.zapstore.app import androidx.lifecycle.SavedStateHandle import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import dev.zapstore.purplequartz.QueryOptions -import dev.zapstore.purplequartz.QueryState -import dev.zapstore.purplequartz.QueryPhase -import dev.zapstore.purplequartz.RemoteMode -import dev.zapstore.purplequartz.SourceMode +import dev.zapstore.iolite.QueryOptions +import dev.zapstore.iolite.QueryState +import dev.zapstore.iolite.QueryPhase +import dev.zapstore.iolite.RemoteMode +import dev.zapstore.iolite.SourceMode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow