Hey folks �� — wanted to float an idea for discussion before 1.0 hardens the surface; happy to drop it if it's not the direction you want to go.
Observation
Today the library ships as one fat artifact (org.meshtastic:mqtt-client) whose commonMain bakes in both TcpTransport (ktor-network + ktor-network-tls) and WebSocketTransport (ktor-client-websockets, plus the wasmJs variant). Every consumer pulls both onto their classpath even when they only use one:
- A serverless / edge consumer that only speaks WSS still pays for ktor-network-tls.
- A backend consumer that only speaks TCP+TLS still pays for ktor-client-websockets.
Not a huge deal in absolute bytes, but it shows up in cold-start, Kotlin/JS bundle size, and dependency-graph noise.
Proposal
Split :library along the boundary that already exists internally:
:core — packets, codec, client, connection, QoS state machines (essentially today's commonMain minus transport impls). No ktor-network / ktor-websocket deps.
:transport-tcp — TcpTransport, depends on :core + ktor-network + ktor-network-tls.
:transport-ws — WebSocketTransport for non-web targets, depends on :core + ktor-client-websockets.
:transport-ws-wasm — wasmJs WebSocket impl if it warrants its own module (or fold into :transport-ws via source sets if cleaner).
- Optional
:mqtt-client meta artifact — depends on all three so existing consumers keep their one-liner add and nothing breaks at the GAV level.
Prior art in the KMP / Kotlin ecosystem
This shape is well-trodden:
- Ktor client ships
ktor-client-core plus per-engine artifacts (ktor-client-cio, ktor-client-okhttp, ktor-client-darwin, ktor-client-js, …). Consumers pick exactly the engine they need.
- SQLDelight ships a core runtime plus per-driver modules (
sqlite-driver, android-driver, native-driver, web-worker-driver).
- Coil 3 ships
coil-core separate from network backends (coil-network-ktor3, coil-network-okhttp).
- OkHttp keeps
okhttp and the optional okhttp-tls / okhttp-sse extensions as separate coordinates.
Common thread: a small core defines the abstraction, and each I/O backend is its own coordinate. Consumers compose what they need; libraries don't pay for backends they don't use.
What tends to be painful in practice (worth flagging upfront):
- More publication coordinates to keep in sync (versions, signing, Sonatype staging).
- More
build.gradle.kts files; convention plugins help but it's still more surface.
- More ABI baselines and more
Module.md files for Dokka.
- Build matrix grows; CI time goes up unless you're careful with task avoidance.
Honest tradeoffs
Pro
- Smaller dependency footprint per consumer.
- Architecture is enforceable in the build graph (e.g., a Konsist rule that
:core cannot depend on a transport module).
- ABI baselines scoped per module → cleaner SemVer signal.
- Future transports (QUIC? MQTT-SN?) don't tax existing consumers.
Con
- More publication coordinates.
- Consumers using both transports add two deps instead of one (mitigated by the meta artifact).
- More build files / Dokka modules / API dumps to maintain.
- Renaming/repurposing
org.meshtastic:mqtt-client would be breaking unless the meta artifact keeps that exact GAV.
Migration path
Keep org.meshtastic:mqtt-client as a meta artifact that api-exports :core + :transport-tcp + :transport-ws. Existing consumers see zero change. Power users opt into the slim modules when they care.
Timing
This is much easier as a 0.x conversation than a post-1.0 one — wanted to raise it now while the GAV story is still flexible.
Happy to draft a PR (module split + meta artifact wiring + a build-graph guard) if the maintainers are interested in the direction. If you'd rather keep the single-artifact shape, totally reasonable — just wanted to put the option on the table.
Hey folks �� — wanted to float an idea for discussion before 1.0 hardens the surface; happy to drop it if it's not the direction you want to go.
Observation
Today the library ships as one fat artifact (
org.meshtastic:mqtt-client) whosecommonMainbakes in bothTcpTransport(ktor-network + ktor-network-tls) andWebSocketTransport(ktor-client-websockets, plus the wasmJs variant). Every consumer pulls both onto their classpath even when they only use one:Not a huge deal in absolute bytes, but it shows up in cold-start, Kotlin/JS bundle size, and dependency-graph noise.
Proposal
Split
:libraryalong the boundary that already exists internally::core— packets, codec, client, connection, QoS state machines (essentially today'scommonMainminus transport impls). No ktor-network / ktor-websocket deps.:transport-tcp—TcpTransport, depends on:core+ktor-network+ktor-network-tls.:transport-ws—WebSocketTransportfor non-web targets, depends on:core+ktor-client-websockets.:transport-ws-wasm— wasmJs WebSocket impl if it warrants its own module (or fold into:transport-wsvia source sets if cleaner).:mqtt-clientmeta artifact — depends on all three so existing consumers keep their one-liner add and nothing breaks at the GAV level.Prior art in the KMP / Kotlin ecosystem
This shape is well-trodden:
ktor-client-coreplus per-engine artifacts (ktor-client-cio,ktor-client-okhttp,ktor-client-darwin,ktor-client-js, …). Consumers pick exactly the engine they need.sqlite-driver,android-driver,native-driver,web-worker-driver).coil-coreseparate from network backends (coil-network-ktor3,coil-network-okhttp).okhttpand the optionalokhttp-tls/okhttp-sseextensions as separate coordinates.Common thread: a small core defines the abstraction, and each I/O backend is its own coordinate. Consumers compose what they need; libraries don't pay for backends they don't use.
What tends to be painful in practice (worth flagging upfront):
build.gradle.ktsfiles; convention plugins help but it's still more surface.Module.mdfiles for Dokka.Honest tradeoffs
Pro
:corecannot depend on a transport module).Con
org.meshtastic:mqtt-clientwould be breaking unless the meta artifact keeps that exact GAV.Migration path
Keep
org.meshtastic:mqtt-clientas a meta artifact thatapi-exports:core+:transport-tcp+:transport-ws. Existing consumers see zero change. Power users opt into the slim modules when they care.Timing
This is much easier as a 0.x conversation than a post-1.0 one — wanted to raise it now while the GAV story is still flexible.
Happy to draft a PR (module split + meta artifact wiring + a build-graph guard) if the maintainers are interested in the direction. If you'd rather keep the single-artifact shape, totally reasonable — just wanted to put the option on the table.