Skip to content
Merged

Main #12

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ jobs:
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PRIVATE_KEY_PASS }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PRIVATE_KEY_PASS }}
56 changes: 38 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ A simple EventBus implementation based on Kotlin SharedFlow and inspired by

## Concept and Usages

### Setup

Use MavenCentral in `settings.gradle.kts`:

```kotlin
repositories {
mavenCentral()
}
```

Include in application `build.gradle.kts`:

```kotlin
dependencies {
implementation("org.holance:ktbus:{version}")
}
```

Make sure to replace `{version}` with the version of the library.

### Publish/Subscribe

```mermaid
Expand Down Expand Up @@ -73,7 +93,7 @@ n2 -- Response --> n1

```kotlin

data class ComputeSquareEvent(val value: Int)
data class ComputeSquareRequest(val value: Int) : Request<ComputeSquareResult>
data class ComputeSquareResult(val value: Int)

class MathClass {
Expand All @@ -84,33 +104,33 @@ class MathClass {
fun tearDown() {
bus.unsubscribe(this)
}
@Subscribe
fun handleRequest(event: Request<ComputeSquareEvent, ComputeSquareResult>) {
// Process event and create a response with type Event2
event.setResult(ComputeSquareResult(event.data.value * event.data.value))
@RequestHandler
fun computeSquare(event: ComputeSquareRequest) : ComputeSquareResult {
return ComputeSquareResult(event.value * event.value)
}
}

val bus = KtBus.getDefault()

bus.request<ComputeSquareEvent, ComputeSquareResult>(ComputeSquareEvent(5)) { result: Response<ComputeSquareResult> ->
when (result) {
is Response.Success -> {
assert(result.data.value == 25)
}
is Response.Error -> {
println("Error: ${result.error}")
}
is Response.Timeout -> {
println("Timeout")
}
}
try {
val result = bus.request(ComputeSquareRequest(5))
assert(result.value == 25)
}
catch (e: RequestException) {
// Handle request handler exception
}
catch (e: NoRequestHandlerException) {
// Handle no request handler exception
}
catch (e: RequestTimeoutException) {
// Handle request timeout exception
}

```

### Use Channel

#### Compile time Channel
#### Compile-Time Channel

```kotlin
data class SomeEvent(val value: Int)
Expand Down
21 changes: 4 additions & 17 deletions gitversion.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
next-version: 1.0.0
workflow: GitFlow/v1
mode: ContinuousDeployment

next-version: 1.1.0
branches:
develop:
regex: ^develop
mode: ContinuousDelivery
label: alpha
increment: Minor
track-merge-target: true
source-branches: []
tracks-release-branches: true
is-release-branch: false
is-main-branch: false
pre-release-weight: 0

release:
regex: ^release
label: beta
increment: Minor
is-release-branch: false
is-main-branch: false

main:
regex: ^main
mode: ContinuousDelivery
is-release-branch: true
is-main-branch: true
2 changes: 0 additions & 2 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinMultiplatform
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
Expand Down
10 changes: 9 additions & 1 deletion lib/src/commonMain/kotlin/org/holance/ktbus/Annotation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ import kotlin.reflect.KClass
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Subscribe(
annotation class Subscribe(
val scope: DispatcherTypes = DispatcherTypes.Unconfined,
val channel: String = DefaultChannelFactory.DEFAULT_CHANNEL,
val channelFactory: KClass<out ChannelFactory> = DefaultChannelFactory::class
)

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class RequestHandler(
val scope: DispatcherTypes = DispatcherTypes.Unconfined,
val channel: String = DefaultChannelFactory.DEFAULT_CHANNEL,
val channelFactory: KClass<out ChannelFactory> = DefaultChannelFactory::class
Expand Down
129 changes: 8 additions & 121 deletions lib/src/commonMain/kotlin/org/holance/ktbus/Events.kt
Original file line number Diff line number Diff line change
@@ -1,123 +1,10 @@
package org.holance.ktbus

import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean

// Helper data class for the request function's result
sealed class Response<out R> {
data class Success<out R>(val data: R) : Response<R>()
data class Error(val message: String, val exception: Throwable? = null) :
Response<Nothing>()

object Timeout : Response<Nothing>()
}

@Suppress("unused")
// Request event - carries data and a unique ID
data class Request<T, E>(
val requestId: String = UUID.randomUUID().toString(), // Unique ID for correlation
val data: T,
val bus: KtBus,
val channel: String,
) {
private val resultSent = AtomicBoolean(false)

/**
* Sets the result of this RequestEvent.
*
* This method attempts to set the result of the event. If the result or error has already been set,
* it throws an IllegalStateException.
*
* @param result The result to be set.
* @throws IllegalStateException if the result or error has already been set for this RequestEvent.
*/
fun setResult(result: E) {
if (trySetResult(result).not()) {
throw IllegalStateException("Result or error already set for this RequestEvent")
}
}

/**
* Attempts to set the result of the operation.
*
* This function is designed to be called once to deliver the result of an asynchronous operation.
*
* @param result The result of the operation to be sent.
* @return `true` if the result was successfully set and sent, `false` if the result had already
* been set (and therefore this call did nothing).
*/
fun trySetResult(result: E): Boolean {
if (resultSent.compareAndSet(false, true)) {
val response = ResponseEvent<E>(requestId, result)
bus.post(response, channel)
return true
} else {
return false
}
}


/**
* Indicates whether a result has already been set.
*
*
* @return `true` if this is the first call and the result is now marked as set,
* `false` if a result has already been set.
*/
fun hasResult(): Boolean {
return resultSent.compareAndSet(false, true)
}

/**
* Sets an error message for the request event.
*
* This function attempts to set the provided [message] as an error for the current request event.
* If an error or a successful result has already been set, it throws an [IllegalStateException].
*
* @param message The error message to be set. Must not be null.
* @throws IllegalStateException If a result or error has already been set for this request event.
*/
fun setError(message: String) {
if (trySetError(message).not()) {
throw IllegalStateException("Result or error already set for this RequestEvent")
}
}

/**
* Attempts to set an error message for the current operation.
*
* This function is designed to be called only once to report an error.
*
* @param message The error message to be included in the response.
* @return `true` if the error was successfully set, `false` otherwise (meaning
* a response has already been set).
*
* @see ResponseEvent
* @see resultSent
* @see bus
* @see channel
* @see requestId
*
* @sample
* // Example usage:
* if(!trySetError("Something went wrong")){
* //log error: could not send error because response already sent
* }
*/
fun trySetError(message: String): Boolean {
if (resultSent.compareAndSet(false, true)) {
val response = ResponseEvent<E>(requestId, error = message)
bus.post(response, channel)
return true
} else {
return false
}
}
}

// Response event - carries data and the ID of the request it's responding to
internal data class ResponseEvent<E>(
val correlationId: String, // Matches the requestId of the RequestEvent
val data: E? = null,
val error: String? = null
)
// --- EventBus Implementation ---
/** Marker interface for request objects */
interface Request<R : Any> // R is the expected Response type

/** Custom exception for request errors */
open class RequestException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
class NoRequestHandlerException(message: String) : RequestException(message)
class RequestTimeoutException(message: String) : RequestException(message)
Loading