mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Rename PurpleQuartz to Iolite.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# Iolite requires no consumer-specific shrinking rules.
|
||||
+7
-7
@@ -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 {
|
||||
+14
-14
@@ -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<Filter>, options: QueryOptions = QueryOptions.local()): Flow<QueryState> = 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 <T> 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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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<NormalizedRelayUrl>,
|
||||
private val cacheDuration: Duration = 6.hours,
|
||||
private val resolveTimeout: Duration = 10.seconds,
|
||||
+1
-1
@@ -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
|
||||
+2
-2
@@ -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(
|
||||
+1
-1
@@ -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
|
||||
+3
-3
@@ -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<QueryState> =
|
||||
internal fun Iolite.query(filter: Filter, source: QuerySource): Flow<QueryState> =
|
||||
query(filter, source.options)
|
||||
|
||||
internal fun PurpleQuartz.query(filters: List<Filter>, source: QuerySource): Flow<QueryState> =
|
||||
internal fun Iolite.query(filters: List<Filter>, source: QuerySource): Flow<QueryState> =
|
||||
query(filters, source.options)
|
||||
+12
-12
@@ -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<QueryState>()
|
||||
@@ -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<QueryState>()
|
||||
@@ -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<QueryState>()
|
||||
@@ -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<QueryState>,
|
||||
): Job = launch {
|
||||
+7
-7
@@ -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<NormalizedRelayUrl>? = 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<NormalizedRelayUrl>? = null
|
||||
+10
-10
@@ -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),
|
||||
+26
-26
@@ -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<QueryState>()
|
||||
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<QueryState>()
|
||||
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<QueryState>()
|
||||
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<QueryState>()
|
||||
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<QueryState>()
|
||||
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<QueryState>,
|
||||
): Job = launch {
|
||||
+1
-1
@@ -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
|
||||
+6
-6
@@ -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<QueryState>()
|
||||
@@ -1 +0,0 @@
|
||||
# PurpleQuartz requires no consumer-specific shrinking rules.
|
||||
+1
-1
@@ -15,4 +15,4 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
rootProject.name = "zapstore"
|
||||
include(":purplequartz")
|
||||
include(":iolite")
|
||||
|
||||
@@ -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<String, Flow<ProfileInfo?>>()
|
||||
private val appAuthorFlows = ConcurrentHashMap<String, Flow<String?>>()
|
||||
private val c1AuthorFlows = ConcurrentHashMap<String, Flow<String?>>()
|
||||
private val relaySigner = MutableStateFlow<String?>(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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
|
||||
class ZapstoreApplication : Application() {
|
||||
val catalogRepository: CatalogRepository by lazy {
|
||||
PurpleQuartzCatalogRepository(this)
|
||||
IoliteCatalogRepository(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user