feat: add jvm target (desktop / server)#13
Merged
Conversation
Adds a `jvm` target to the Reachable KMP library, covering desktop apps
and server-side JVM services on JVM 21+. The published jar is
architecture-neutral bytecode, so the ARM-only rule (CLAUDE.md §1)
constrains only the native slices, not this jar — Linux/Windows/Intel
hosts running it are fine.
Implementation: the JDK has no connectivity-change callback and no OS
validation probe (unlike Apple's nw_path_monitor / Android's
NET_CAPABILITY_VALIDATED), so JvmReachability polls
java.net.NetworkInterface on the base-class scope (default 5s) and maps
each snapshot via a pure, unit-testable mapJvmInterfaces. isReachable is
therefore best-effort ("a non-loopback interface is up with a routable
address"), captive portals are not detected, isDataMetered is always
false, and transport is name-inferred (Transport.Other when ambiguous,
e.g. macOS en0). Host-only container/hypervisor bridges (docker0,
vmnet*, …) are filtered so a Docker-running laptop reads offline in
airplane mode; VPN tunnels (utun*) count. No HTTP probe by design.
Build wiring is one line — jvm() in the convention plugin slots into the
existing KotlinJvmTarget JVM_21 block (originally written for Android),
and the expect seam being a single createSharedReachability() function
made this a pure addition with zero changes to common logic.
Docs: new platforms/jvm.md plus JVM coverage threaded through the README
(badge, platform table, launch sequence), CLAUDE.md §1/§4, mise.toml,
CI comments, commonMain KDoc, and the index/installation/getting-started/
api-design/lifecycle/validated-vs-available pages.
Verified: ./gradlew :reachable:check :reachable-testing:check (jvmTest +
compileKotlinJvm ran and passed; ktlint/detekt clean; iOS-sim, macOS,
Android-host still green), :reachable:linkDebugFrameworkIosArm64 links
clean, and python3 docs/check.py passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a
jvmtarget to the Reachable KMP library, covering desktop apps (Compose Desktop, Swing, JavaFX) and server-side JVM services on JVM 21+. SameReachabilityAPI as every other platform.The published jar is architecture-neutral bytecode, so the ARM-only rule (CLAUDE.md §1) constrains only the native slices — Linux/Windows/Intel hosts running the jar are fine, which is the whole point of having a JVM target alongside the native ones.
How it works (and the honest caveat)
The JDK has no connectivity-change callback and no OS validation probe — unlike Apple's
nw_path_monitoror Android'sNET_CAPABILITY_VALIDATED. SoJvmReachabilitypollsjava.net.NetworkInterfaceon the base-class scope (default 5 s) and maps each snapshot via a pure, unit-testablemapJvmInterfaces.That means
isReachableis best-effort, documented as such everywhere it surfaces:isDataMeteredis alwaysfalse(no JDK metering signal).Transport.Otherwhen ambiguous (notably macOSen0, which can be Wi-Fi or wired).docker0,veth*,vmnet*, …) are filtered, so a Docker-running laptop reads offline in airplane mode. VPN tunnels (utun*) count as reachable.Why it's a small change
Build wiring is one line —
jvm()in thereachable.kmp-libraryconvention plugin slots straight into the existingtargets.withType<KotlinJvmTarget>JVM_21 block (originally written for Android's JVM compilation). Theexpectseam being a singlecreateSharedReachability()function (CLAUDE.md §4) made this a pure addition with zero changes to common logic — the JVMactualis one line, exactly like Apple's.Files
Implementation (
reachable/src/jvmMain/)JvmReachability.kt— poll-loopStateFlowReachabilitysubclass, with an injected interface source for testing.internal/Mapping.jvm.kt+internal/JvmNetworkInterface.kt— pure interface→status mapping (mirrors the existingMapping.ktsplit).Reachability.jvm.kt— publicReachability(pollInterval: Duration = 5.seconds)factory.internal/SharedReachabilityHolder.jvm.kt— one-linecreateSharedReachability()actual.Tests (
reachable/src/jvmTest/) — 14 mapping tests (classification, bridge exclusion, transport priority, metering-always-false) + 4 poll-loop behavior tests (change detection, single-axis flows, close-stops-loop, real-table smoke test).Docs & meta — new
docs/platforms/jvm.md+ nav; JVM coverage threaded through README,CLAUDE.md§1/§4,mise.toml(build:jvmtask), CI comment, commonMain KDoc, and the index/installation/getting-started/api-design/lifecycle/validated-vs-available pages. Two LESSONS entries (D-004, D-005).Verification
./gradlew :reachable:check :reachable-testing:check—:reachable:jvmTest+:reachable:compileKotlinJvmran and passed (confirms the target is wired intocheck); ktlint/detekt clean on the new source sets; iOS-sim, macOS, and Android-host tests still green../gradlew :reachable:linkDebugFrameworkIosArm64— links clean (no regression to the K/N build).python3 docs/check.py— passes; all new cross-links and the#jvmanchor resolve.Reviewer notes
pollIntervalconstructor arg (default 5 s);Reachability.shareduses the default and starts eagerly like the Apple singleton.Dispatchers.Default(no virtual-time scheduler reaches that dispatcher) and let Turbine suspend — noThread.sleep.🤖 Generated with Claude Code