Skip to content

Commit 8ada7fd

Browse files
authored
3.3.0-alpha02 (#192)
2 parents 6c33a8e + cf8ff13 commit 8ada7fd

File tree

70 files changed

+1893
-277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1893
-277
lines changed

.idea/runConfigurations/Debugger_Desktop.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/Debugger_Run_and_Detach.xml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.junie/.onboarding_migrated

Whitespace-only changes.

.junie/memory/errors.md

Whitespace-only changes.

.junie/memory/feedback.md

Whitespace-only changes.

.junie/memory/tasks.md

Whitespace-only changes.

core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StoreConfiguration.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package pro.respawn.flowmvi.api
22

33
import kotlinx.coroutines.channels.BufferOverflow
4+
import pro.respawn.flowmvi.annotation.InternalFlowMVIAPI
45
import pro.respawn.flowmvi.dsl.StoreConfigurationBuilder
56
import pro.respawn.flowmvi.logging.StoreLogger
67
import kotlin.coroutines.CoroutineContext
8+
import kotlin.uuid.Uuid
79

810
/**
911
* A configuration of the [Store].
@@ -27,6 +29,9 @@ public data class StoreConfiguration<out S : MVIState> internal constructor(
2729
val name: String?,
2830
) {
2931

32+
@InternalFlowMVIAPI
33+
val id: Uuid = Uuid.random()
34+
3035
@Deprecated(
3136
"Please use the StateStrategy directly",
3237
ReplaceWith("this.stateStrategy is StateStrategy.Atomic")

debugger/debugger-client/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ kotlin {
1010
this,
1111
// not supported by all needed ktor artifacts?
1212
watchOs = false,
13-
wasmJs = false,
13+
wasmJs = true,
1414
wasmWasi = false,
1515
)
1616
}

debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebugClientStore.kt

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import pro.respawn.flowmvi.debugger.model.ClientEvent.StoreConnected
2929
import pro.respawn.flowmvi.debugger.model.ServerEvent
3030
import pro.respawn.flowmvi.dsl.store
3131
import pro.respawn.flowmvi.logging.StoreLogLevel
32+
import pro.respawn.flowmvi.logging.StoreLogger
33+
import pro.respawn.flowmvi.logging.invoke
3234
import pro.respawn.flowmvi.logging.log
3335
import pro.respawn.flowmvi.plugins.enableLogging
3436
import pro.respawn.flowmvi.plugins.init
@@ -40,17 +42,17 @@ import kotlin.uuid.Uuid
4042
internal typealias DebugClientStore = Store<EmptyState, ClientEvent, ServerEvent>
4143

4244
internal fun debugClientStore(
43-
clientName: String,
45+
clientId: Uuid,
46+
clientKey: String?,
4447
client: HttpClient,
4548
host: String,
4649
port: Int,
4750
reconnectionDelay: Duration,
4851
logEvents: Boolean = false,
4952
) = store(EmptyState) {
50-
val id = Uuid.random()
5153
val session = MutableStateFlow<DefaultClientWebSocketSession?>(null)
5254
configure {
53-
name = "${clientName}Debugger"
55+
name = clientKey ?: "Debugger"
5456
coroutineContext = Dispatchers.Default
5557
debuggable = true
5658
parallelIntents = false // ensure the order of events matches server's expectations
@@ -73,23 +75,19 @@ internal fun debugClientStore(
7375
}
7476
},
7577
) {
76-
log(StoreLogLevel.Trace) { "Starting connection at $host:$port/$id" }
7778
val _ = client.webSocketSession(
7879
method = HttpMethod.Get,
7980
host = host,
8081
port = port,
81-
path = "/$id",
82+
path = "/$clientId",
8283
).apply {
8384
session.update {
8485
it?.close()
8586
this
8687
}
87-
sendSerialized<ClientEvent>(StoreConnected(clientName, id))
88+
sendSerialized<ClientEvent>(StoreConnected(clientKey, clientId))
8889
log(StoreLogLevel.Trace) { "Established connection to ${call.request.url}" }
89-
awaitEvents {
90-
log(StoreLogLevel.Trace) { "Received event: $it" }
91-
if (it.storeId == id) action(it)
92-
}
90+
awaitEvents(config.logger) { if (it.storeId == clientId) action(it) }
9391
}
9492
}
9593
}
@@ -124,6 +122,21 @@ private inline fun CoroutineScope.launchConnectionLoop(
124122
}
125123
}
126124

127-
private suspend inline fun DefaultClientWebSocketSession.awaitEvents(onEvent: (ServerEvent) -> Unit) {
128-
while (isActive) onEvent(receiveDeserialized<ServerEvent>())
125+
@Suppress("TooGenericExceptionCaught") // intentional - resilient event loop
126+
private suspend inline fun DefaultClientWebSocketSession.awaitEvents(
127+
log: StoreLogger,
128+
onEvent: (ServerEvent) -> Unit
129+
) {
130+
while (isActive) {
131+
try {
132+
val event = receiveDeserialized<ServerEvent>()
133+
log(StoreLogLevel.Trace) { "Received event: $event" }
134+
onEvent(event)
135+
} catch (e: CancellationException) {
136+
throw e
137+
} catch (e: Exception) {
138+
log(e, StoreLogLevel.Warn)
139+
continue
140+
}
141+
}
129142
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pro.respawn.flowmvi.debugger.client
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.request.post
5+
import io.ktor.client.request.setBody
6+
import io.ktor.http.ContentType
7+
import io.ktor.http.contentType
8+
import io.ktor.http.path
9+
import io.ktor.utils.io.CancellationException
10+
import pro.respawn.flowmvi.debugger.DebuggerDefaults
11+
import pro.respawn.flowmvi.metrics.MetricsSink
12+
13+
/**
14+
* Creates a [MetricsSink] that sends store metrics to the FlowMVI Debugger server.
15+
*
16+
* @param client The [HttpClient] to use for sending metrics.
17+
* @param host The debugger server host. Defaults to [DebuggerDefaults.ClientHost].
18+
* @param port The debugger server port. Defaults to [DebuggerDefaults.Port].
19+
* @param onError A callback invoked when an error occurs while sending metrics. By default, errors are swallowed.
20+
* @return A [MetricsSink] that forwards metrics to the debugger server.
21+
*/
22+
public fun DebuggerSink(
23+
client: HttpClient,
24+
host: String = DebuggerDefaults.ClientHost,
25+
port: Int = DebuggerDefaults.Port,
26+
onError: (e: Exception) -> Unit = { /* swallow */ },
27+
): MetricsSink = MetricsSink { snapshot ->
28+
val storeId = snapshot.meta.storeId ?: return@MetricsSink
29+
runCatching {
30+
client.post {
31+
url.host = host
32+
url.port = port
33+
contentType(ContentType.Application.Json)
34+
url.path(storeId.toString(), "metrics")
35+
setBody(snapshot)
36+
}
37+
}.onFailure {
38+
when (it) {
39+
!is Exception, is CancellationException -> throw it
40+
else -> onError(it)
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)