Skip to content
Merged
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
41 changes: 39 additions & 2 deletions .github/workflows/ci-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'scripts/**'
- '.github/workflows/**'
- 'Package.swift'
- 'src/Cocoar.SignalARRR.Kotlin/**'
workflow_dispatch:
inputs:
checkout_ref:
Expand Down Expand Up @@ -108,6 +109,33 @@ jobs:
- name: Run Swift tests
run: swift test

test-kotlin:
name: Test Kotlin client
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.checkout_ref || github.sha }}

- name: Setup Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: Build and test Kotlin client
working-directory: src/Cocoar.SignalARRR.Kotlin
run: ./gradlew build --console=plain

integration-test:
name: Cross-platform integration tests
runs-on: macos-latest
Expand Down Expand Up @@ -136,7 +164,16 @@ jobs:
working-directory: src/Cocoar.SignalARRR.Typescript
run: npm install && npm run build

- name: Run integration tests (.NET + TypeScript + Swift)
- name: Setup Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: Run integration tests (.NET + TypeScript + Swift + Kotlin)
env:
DOTNET_TARGET_FRAMEWORK: net10.0
DOTNET_TEST_FILTER: "(Type!=Performance)&(FullyQualifiedName!~BackplaneIntegrationTests)"
Expand All @@ -145,7 +182,7 @@ jobs:
build-artifacts:
name: Build alpha artifacts
runs-on: ubuntu-latest
needs: [test-multiplatform, test-swift, integration-test]
needs: [test-multiplatform, test-swift, test-kotlin, integration-test]
permissions:
packages: write
contents: read
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/ci-pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'scripts/**'
- '.github/workflows/**'
- 'Package.swift'
- 'src/Cocoar.SignalARRR.Kotlin/**'

jobs:
test-multiplatform:
Expand Down Expand Up @@ -117,6 +118,34 @@ jobs:
- name: Run Swift tests
run: swift test

test-kotlin:
name: Test Kotlin client
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

# Unit tests and the KSP processor; the integration tests run in the integration job below,
# against the same server as the other clients.
- name: Build and test Kotlin client
working-directory: src/Cocoar.SignalARRR.Kotlin
run: ./gradlew build --console=plain

integration-test:
name: Integration tests (all frameworks)
runs-on: macos-latest
Expand All @@ -143,6 +172,15 @@ jobs:
with:
node-version: 22

- name: Setup Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: Build TypeScript client
working-directory: src/Cocoar.SignalARRR.Typescript
run: npm install && npm run build
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Kotlin client** (`dev.cocoar:signalarrr`, `dev.cocoar:signalarrr-ksp`) for Android and the JVM — the fifth client, built the way the Swift one is: a full SignalR client of its own (negotiate, WebSockets, Server-Sent Events and Long Polling with fallback, JSON and MessagePack, keep-alive, server timeout, automatic reconnection) rather than a wrapper around Microsoft's Java client, which has no reconnect, no SSE and leaks RxJava into the API. Coroutines and `Flow` are the async model: `invoke` and `send` suspend, `stream` returns a cold `Flow` whose cancellation cancels the server-side stream. The full SignalARRR surface is there — token challenges, structured errors with codes (`HARRRException.code`), server-to-client handlers with `onServerMethod`, `registerInterface` and stream handlers, HTTP stream references in both directions (`ByteArray`, `File`, `InputStream` arguments are uploaded, `Stream` parameters arrive as bytes), and cancellation propagation in the idiomatic form: the handler's coroutine is cancelled when the server cancels, so `delay` and every other suspending call just stops. `@HubProxy` interfaces get a typed proxy generated at build time by a KSP processor; `ProxyKind` addresses contract interfaces, `ServerMethods` classes and hub methods alike, and Kotlin's camelCase members map to .NET's PascalCase by default. The plain `SignalRClient` underneath talks to any SignalR hub. Verified by 66 integration tests against the shared test server over all three transports, and by the unit suite. Documented under the new *Kotlin Client* section.

---

## [5.1.0] - 2026-09-05
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Typed bidirectional RPC over ASP.NET Core SignalR.

Server and client call each other's methods through shared interfaces — with compile-time proxy generation, streaming, cancellation propagation, and ASP.NET Core authorization. Clients available for **.NET**, **TypeScript/JavaScript**, and **Swift**.
Server and client call each other's methods through shared interfaces — with compile-time proxy generation, streaming, cancellation propagation, and ASP.NET Core authorization. Clients available for **.NET**, **TypeScript/JavaScript**, **Swift**, and **Kotlin/Android**.

> **[Read the full documentation](https://docs.cocoar.dev/signalarrr/)**

Expand Down Expand Up @@ -39,6 +39,13 @@ npm install @cocoar/signalarrr
.package(url: "https://github.com/cocoar-dev/Cocoar.SignalARRR.git", from: "5.0.0")
```

### Kotlin (Android / JVM)

```kotlin
implementation("dev.cocoar:signalarrr:5.2.0")
ksp("dev.cocoar:signalarrr-ksp:5.2.0") // typed proxies via @HubProxy
```

## Quick Start

Define shared interfaces, set up the server, and call methods with full type safety:
Expand Down Expand Up @@ -67,6 +74,13 @@ const history = await connection.invoke<string[]>('ChatMethods.GetHistory');
let chat = connection.getTypedMethods(IChatHubProxy.self)
```

```kotlin
// Kotlin client — KSP generates the proxy from a @HubProxy interface
@HubProxy(name = "MyApp.Contracts.IChatHub") interface IChatHub { ... }
val chat = connection.getTypedMethods(IChatHubProxy)
chat.sendMessage("Alice", "Hello!")
```

For full setup guides, streaming, authorization, and server-to-client calls, see the **[documentation](https://docs.cocoar.dev/signalarrr/)**.

## Features
Expand All @@ -79,7 +93,7 @@ For full setup guides, streaming, authorization, and server-to-client calls, see
- **CancellationToken propagation** — server can cancel client operations remotely
- **Authorization** — method-level, class-level, and hub-level `[Authorize]`
- **Server-to-client calls from anywhere** — inject `ClientManager` in controllers, background services, etc.
- **Four clients** — .NET, .NET Framework, TypeScript/JavaScript, Swift
- **Five clients** — .NET, .NET Framework, TypeScript/JavaScript, Swift, Kotlin/Android
- **Typed broadcasts** — `WithHub<T>().WithGroup().SendAsync<T>()` for groups and filtered clients
- **Multi-node backplane** — opt-in scale-out over Redis/Valkey/Garnet, or over the PostgreSQL you already run; with cluster subjects, server streams fed by an in-process observable become cluster-wide too

Expand Down Expand Up @@ -173,6 +187,7 @@ What happens to in-flight work when a connection drops — worth knowing before
| .NET Framework (client) | 4.6.2+ (via `Cocoar.SignalARRR.Client.FullFramework`) |
| TypeScript / JavaScript | Node.js 22 / modern browsers |
| Swift (iOS / macOS) | Swift 5.10+, iOS 14+ / macOS 11+ |
| Kotlin (Android / JVM) | Kotlin 2.0+, Android 5+ (API 21) / JVM 8+ |

## Building from Source

Expand All @@ -186,6 +201,9 @@ cd src/Cocoar.SignalARRR.Typescript && npm install && npm run build

# Swift
swift build && swift test

# Kotlin (needs a JDK 21 on JAVA_HOME)
cd src/Cocoar.SignalARRR.Kotlin && ./gradlew build
```

## License
Expand Down
20 changes: 20 additions & 0 deletions scripts/run-integration-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# ./scripts/run-integration-tests.sh dotnet # .NET only
# ./scripts/run-integration-tests.sh swift # Swift only
# ./scripts/run-integration-tests.sh typescript # TypeScript only
# ./scripts/run-integration-tests.sh kotlin # Kotlin only

set -euo pipefail

Expand Down Expand Up @@ -87,6 +88,25 @@ if [ "$FILTER" = "all" ] || [ "$FILTER" = "typescript" ]; then
fi
fi

# --- Kotlin Tests ---

if [ "$FILTER" = "all" ] || [ "$FILTER" = "kotlin" ]; then
KOTLIN_DIR="$REPO_ROOT/src/Cocoar.SignalARRR.Kotlin"
if [ -f "$KOTLIN_DIR/gradlew" ] && { command -v java &>/dev/null || [ -n "${JAVA_HOME:-}" ]; }; then
echo ""
echo "=== Running Kotlin Integration Tests ==="
if (cd "$KOTLIN_DIR" && ./gradlew :signalarrr-integration-tests:test --console=plain -q); then
echo "Kotlin tests: PASSED"
else
echo "Kotlin tests: FAILED"
FAILED=1
fi
else
echo ""
echo "=== Skipping Kotlin Tests (no JDK available) ==="
fi
fi

# --- Summary ---

echo ""
Expand Down
22 changes: 15 additions & 7 deletions skills/signalarrr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: signalarrr
description: >
Typed bidirectional RPC over ASP.NET Core SignalR with Cocoar.SignalARRR. Use when working
with HARRR hubs, ServerMethods<T>, [SignalARRRContract] interfaces, HARRRConnection on .NET,
the @cocoar/signalarrr npm client or the CocoarSignalARRR Swift package, server-to-client calls,
the @cocoar/signalarrr npm client, the CocoarSignalARRR Swift package or the dev.cocoar:signalarrr
Kotlin/Android client, server-to-client calls,
ClientManager and IClientQuery, item streaming (IAsyncEnumerable, IObservable, ChannelReader),
HTTP stream references, cancellation propagation, SignalARRR authorization, the Redis or
PostgreSQL backplane, or cluster subjects.
Expand Down Expand Up @@ -35,22 +36,24 @@ bottom says which page answers what.
| `Cocoar.SignalARRR.Common`, `.ProxyGenerator`, `.SourceGenerator` | Referenced transitively; not added by hand |
| `@cocoar/signalarrr` (npm) | TypeScript/JavaScript client: `HARRRConnection`, `invoke`, `send`, `stream`, `onServerMethod` |
| `CocoarSignalARRR` (Swift Package) | Swift client for iOS, macOS, tvOS, watchOS with the `@HubProxy` macro |
| `dev.cocoar:signalarrr`, `dev.cocoar:signalarrr-ksp` (Maven) | Kotlin client for Android and the JVM, coroutines and `Flow`, typed proxies generated by KSP from `@HubProxy` interfaces |

## Things an assistant gets wrong without the docs

- **`send` is fire-and-forget, `invoke` awaits a result, `stream` returns items.** On the .NET
client the return type of the contract method decides: `void`/`Task` → send, `Task<T>` → invoke,
`IAsyncEnumerable<T>`/`IObservable<T>`/`ChannelReader<T>` → stream. On TypeScript and Swift the
caller chooses the call explicitly.
caller chooses the call explicitly; on Kotlin a `@HubProxy` interface decides by signature
(`suspend fun` without result → send, with result → invoke, `Flow<T>` → stream).
- **Every interface a `ServerMethods<T>` class implements is a public RPC surface**, whether or
not it carries `[SignalARRRContract]`. Hold collaborators as fields; do not implement their
interfaces on the class.
- **`WithHub<T>()` returns an `IClientQuery`, not a sequence.** Filter with `WithGroup`, `WithUser`
and `WithAttribute`, which span the cluster; `WithLocalFilter(predicate)` and `LocalClients()`
are node-local by name.
- **Server-to-client calls use SignalR client results natively.** Register handlers with
`RegisterInterface` (.NET), `onServerMethod` (TypeScript) or the interface handler (Swift)
before connecting; `On()` on the raw `HubConnection` is not the contract path.
`RegisterInterface` (.NET), `onServerMethod` (TypeScript, Swift, Kotlin) or the interface
handler before connecting; `On()` on the raw `HubConnection` is not the contract path.
- **A `System.IO.Stream` parameter travels over HTTP, not the WebSocket.** Download endpoints are
one-time; uploads go through `RequestUploadSlot` and `POST /hub/upload/{id}`; slots per
connection are capped.
Expand All @@ -73,7 +76,7 @@ whose description matches the task; they are independent of each other.

### Introduction

- [Getting Started](references/guide/getting-started.md) — Install the packages, define a [SignalARRRContract] interface, implement it in a ServerMethods<T> class on a HARRR hub, and call it from the .NET, TypeScript and Swift clients
- [Getting Started](references/guide/getting-started.md) — Install the packages, define a [SignalARRRContract] interface, implement it in a ServerMethods<T> class on a HARRR hub, and call it from the .NET, TypeScript, Swift and Kotlin clients

### Server

Expand All @@ -100,6 +103,11 @@ whose description matches the task; they are independent of each other.
- [Swift Client Setup](references/guide/swift-client/setup.md) — The CocoarSignalARRR Swift package for iOS, macOS, tvOS and watchOS: installation, connection and options, authentication, invoke / send / stream, events, MessagePack, reconnection policy, transports, logging
- [Typed Proxies & Server Methods](references/guide/swift-client/typed-proxies.md) — Swift typed proxies with the @HubProxy macro, server-to-client and streaming handlers, interface registration, cancellation support, and HTTP stream references

### Kotlin client

- [Kotlin Client Setup](references/guide/kotlin-client/setup.md) — The Kotlin client for Android and the JVM: Gradle setup, create a connection, invoke / send / stream with coroutines and Flow, errors and error codes, MessagePack, token authentication, reconnection, transports, logging, and using the plain SignalR layer
- [Typed Proxies & Server Methods](references/guide/kotlin-client/typed-proxies.md) — Kotlin typed proxies generated by KSP from @HubProxy interfaces (wire names, ProxyKind, @HubMethod), server-to-client handlers with onServerMethod and registerInterface, structured cancellation of handlers, streaming handlers, and HTTP stream references

### Item streaming

- [Server-to-Client Streaming](references/guide/streaming/server-to-client.md) — Stream results from server methods with IAsyncEnumerable<T> (recommended), IObservable<T> or ChannelReader<T>, consume them on the .NET and TypeScript clients, and cancel a stream
Expand All @@ -118,8 +126,8 @@ whose description matches the task; they are independent of each other.

### Reference

- [Packages](references/reference/packages.md) — Every NuGet package, the npm package and the Swift package: purpose and target frameworks, which project references what, typical setups per project role, the dependency graph, and the bundled agent skill
- [Client Comparison](references/reference/client-comparison.md) — Feature matrix of the four clients — .NET, .NET Framework, TypeScript, Swift: platforms, RPC, item streaming, server-to-client RPC, file transfer, authorization, proxies, transports, side-by-side API samples
- [Packages](references/reference/packages.md) — Every NuGet package, the npm package, the Swift package and the Kotlin packages: purpose and target frameworks, which project references what, typical setups per project role, the dependency graph, and the bundled agent skill
- [Client Comparison](references/reference/client-comparison.md) — Feature matrix of the five clients — .NET, .NET Framework, TypeScript, Swift, Kotlin: platforms, RPC, item streaming, server-to-client RPC, file transfer, authorization, proxies, transports, side-by-side API samples
- [API Overview](references/reference/api.md) — The public API surface by package: server registration, HARRR, ServerMethods, ClientContext, ClientManager, IClientQuery, the backplane option builders, IClusterSubject<T>, HARRRConnection on .NET and TypeScript, common types
- [Wire Protocol](references/reference/wire-protocol.md) — The SignalR hub methods and message types SignalARRR uses on the wire — InvokeMessage, InvokeServerMessage, ChallengeAuthentication, CancelTokenFromServer, streaming — with the protocol flows and method name resolution, for custom clients

Expand Down
35 changes: 35 additions & 0 deletions skills/signalarrr/references/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ npm install @cocoar/signalarrr
// Products: CocoarSignalARRR, CocoarSignalARRRMacros
```

```kotlin [Kotlin (build.gradle.kts)]
implementation("dev.cocoar:signalarrr:5.2.0")
ksp("dev.cocoar:signalarrr-ksp:5.2.0") // typed proxies via @HubProxy
```


## 1. Define shared interfaces

Expand Down Expand Up @@ -186,10 +191,40 @@ await connection.onServerMethod("MyApp.Contracts.IChatClient|GetClientName") { _
}
```

## 6. Kotlin client (Android / JVM)

```kotlin
import dev.cocoar.signalarrr.HARRRConnection
import dev.cocoar.signalarrr.HubProxy
import kotlinx.coroutines.flow.Flow

@HubProxy(name = "MyApp.Contracts.IChatHub")
interface IChatHub {
suspend fun sendMessage(user: String, message: String)
suspend fun getHistory(): List<String>
fun streamMessages(): Flow<String>
}

val connection = HARRRConnection.create("https://localhost:5001/chathub")
connection.start()

// Typed calls through the KSP-generated proxy
val chat = connection.getTypedMethods(IChatHubProxy)
chat.sendMessage("Alice", "Hello!")
val history = chat.getHistory()

// Streaming
chat.streamMessages().collect { msg -> println(msg) }

// Handle server-to-client calls. The name is the contract's wire name, "interface|method".
connection.onServerMethod("MyApp.Contracts.IChatClient|GetClientName") { Build.MODEL }
```

## Next steps

- [Why SignalARRR?](https://docs.cocoar.dev/signalarrr/guide/why-signalarrr.html) — what problems SignalARRR solves compared to raw SignalR
- [Hub Setup](./server/hub-setup.md) — HARRR base class and configuration
- [Server Methods](./server/server-methods.md) — organizing hub logic across classes
- [TypeScript Client](./typescript-client/setup.md) — complete TypeScript/JavaScript guide
- [Swift Client](./swift-client/setup.md) — complete Swift/iOS/macOS guide
- [Kotlin Client](./kotlin-client/setup.md) — complete Kotlin/Android guide
Loading