Skip to content

About

Experimental Socket.IO client for Kotlin Multiplatform (Android, JVM, JavaScript, WasmJs, and iOS).

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Socket.IO client for Kotlin Multiplatform

An early Kotlin-first Socket.IO client for Android, JVM, JavaScript, WasmJs, and iOS. It speaks Socket.IO protocol revision 5 over Engine.IO v4. Construction is inert; connect() waits for the namespace handshake.

val client = socketIoClient("https://example.com", scope)
val chat = client.socket("/chat", NamespaceOptions(auth = {
    buildJsonObject { put("token", accessToken()) }
}))

val subscription = chat.on("message") { event ->
    println(event.args)
}
val connection = chat.connect()
println("socket id=${connection.id}, recovered=${connection.recovered}")

chat.emit("message", listOf(SocketValue.Json(JsonPrimitive("hello"))))
val reply = chat.emitWithAck("request", listOf(SocketValue.Json(JsonPrimitive(1))))

subscription.dispose()
client.close()

Import io.socket.kmp.* and kotlinx.serialization.json.* for this example. Keep the supplied CoroutineScope alive while using the client. SocketIoClient.close() closes its Engine.IO connection and its internally created Ktor HttpClient. The overload accepting a configured HttpClient leaves that client under the caller's ownership; install Ktor's WebSockets plugin on it.

Behavior

  • Namespace sockets from one client share an Engine.IO connection. The default starts with HTTP long polling and probes a WebSocket upgrade. Either transport can be selected alone.
  • Set ClientOptions.rememberUpgrade = true to try WebSocket first on the next connection after a successful WebSocket connection or upgrade. A failed WebSocket attempt falls back to the remaining configured transports. The remembered result belongs to that client.
  • SocketValue supports JSON primitives and objects, nested arrays and objects, and binary attachments. Binary input is copied. Incoming packets allow 10 attachments by default; set ClientOptions.maxBinaryAttachments for larger packets.
  • Ordinary emits buffer while disconnected, up to NamespaceOptions.maxBufferedEvents. emitVolatile drops events when the namespace or transport cannot write. An accepted write does not prove server delivery.
  • Incoming events expose a once-only, connection-bound ack. emitWithAck has attempt and total timeouts; retries require maxRetries > 0 and can cause duplicate server effects. ACK calls are serialized in call order, while ordinary emits can proceed independently.
  • Authentication is evaluated for every namespace handshake. If the server enables connection state recovery, the client saves its private session ID and last event offset and reports the outcome in Connection.recovered. Recovery is best effort and depends on the server's retention window and adapter.
  • Initial Engine.IO failures use ClientOptions.reconnection while connect() waits. If connect() times out, it stops those initial retries; established connections continue to use the reconnection policy after transport loss.
  • Socket.state holds the current state; onLifecycle reports transitions. events(name) offers a Flow view. Dispose listener subscriptions when they are no longer needed.
  • onAnyOutgoing observes events when they are handed to the transport, including buffered sends on reconnect and ACK retries. Dropped volatile events and ACK replies are excluded. Dispose its subscription when finished.
  • Set ClientOptions.parser to a SocketIoParser for a custom Socket.IO packet format. encode returns Engine.IO message bodies in write order; createDecoder provides connection-scoped state and receives the configured binary attachment limit. Decoders are reset when their connection ends. A parser shared across clients or sockets must support concurrent encode calls.

This is an initial implementation of the proposed Kotlin surface, not an alias for every Java or JavaScript option. In particular, browser credentials and cross-origin behavior depend on the selected Ktor engine and server configuration. Live Chrome and Android emulator interoperability is covered below; iOS device interoperability has not been run.

Build and verification

./gradlew jvmTest jsNodeTest wasmJsNodeTest \
  compileKotlinIosArm64 compileKotlinIosSimulatorArm64

wasmJsNodeTest executes the common tests as WebAssembly in Node; it does not exercise a browser network stack. jsNodeTest also runs without a live server; the Chrome interoperability tests run under jsBrowserTest.

For live JVM and Chrome interoperability tests against Socket.IO 4.8.3:

cd interop && npm ci && SOCKET_IO_HOST=0.0.0.0 SOCKET_IO_PORT=3000 npm start
# In a second terminal, from the repository root:
SOCKET_IO_TEST_URL=http://127.0.0.1:3000 \
  CHROME_BIN='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' \
  ./gradlew jvmTest jsBrowserTest

Set CHROME_BIN to your Chrome executable if it is elsewhere. The browser tests expect the server on port 3000. The live tests cover polling with WebSocket upgrade, direct WebSocket, JSON and binary ACKs, namespace multiplexing, encoded query parameters, auth, middleware rejection, buffered event order, and server-initiated ACKs. Common tests also include wire fixtures adapted from the Java and JavaScript parser suites and recovery packets replayed before CONNECT.

The native Android test app in android-test-app uses the same Kotlin client. With the server still running, build and install it on an emulator:

./gradlew :android-test-app:assembleDebug
adb install -r android-test-app/build/outputs/apk/debug/android-test-app-debug.apk
adb shell am start -n io.socket.kmp.testapp/.MainActivity --ez autorun true

The default endpoint http://10.0.2.2:3000 reaches the host from the Android emulator. The app runs six live scenarios: direct WebSocket ACKs, polling upgrade, namespace auth and buffering, reconnection after a server-forced transport close, outgoing listeners with a custom parser, and remembered WebSocket upgrade with a polling-first retry after transport loss. See interop/README.md for server controls and a manual browser page.

Migrated protocol tests

Source tests Kotlin tests
Java parser/ParserTest.java, parser/ByteArrayTest.java JavaProtocolParityTest: text headers, namespace and ACK IDs, binary attachment order, nested values, nulls, and decoder reset
Java client/SocketTest.java ClientProtocolParityTest: auth, middleware rejection, and buffered ACK order
JavaScript socket.io-parser/test/parser.js, buffer.js, arraybuffer.js JsProtocolParityTest: protocol v5 wire fixtures, nested binary ACKs, invalid payloads and placeholders, and decoder reset
JavaScript socket.io-client/test/socket.ts, connection-state-recovery.ts SocketRuntimeTest and ClientProtocolParityTest: ACK retry, volatile emits, namespace multiplexing, and replay before recovered CONNECT

Java's three binary-event fixtures without an event name were adapted by prepending "event", which the current JavaScript protocol parser requires. JavaScript-only object behaviors such as toJSON(), custom revivers, cyclic objects, Blob, and typed-array variants are outside the SocketValue model; their binary byte behavior is covered through SocketValue.Binary. Live browser and Android checks complement the common protocol tests; a live iOS device run remains outstanding.

About

Experimental Socket.IO client for Kotlin Multiplatform (Android, JVM, JavaScript, WasmJs, and iOS).

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages