Matt Nelson
09/28/2025, 1:07 PMNode.js synchronous API under the hood.
• Adding the Async API would allow use of Node.js callback API.
• Adding the Async API would allow supporting (in a limited context such as WebWorkers) browser functionality.
For Js/WasmJs I need the suspendCancellableCoroutine function from kotlinx-coroutines
All other targets (Jvm/Native) would simply call the blocking functions from their xxxAsync() actual function definitions
Should I
1. Suck it up and add the kotlinx-coroutines dependency to the Js/WasmJs source sets
2. Expose public functions for Js/WasmJs with a Continuation argument marked as @OptIn(InternalApi::class) and have a separate extension module kmp-file:async with the kotlinx-coroutines dependency and all xxxAsync() functions defined.
3. Something else? Like wrapping the callback function from Kotlin in js("async { block() }")
I would really like to avoid the additional dependency...Zach Klippenstein (he/him) [MOD]
09/28/2025, 4:09 PMimplementation dependency not api (not sure if that gradle distinction translates to web). That said, the coroutines library is effectively part of the standard lib already - most kotlin libraries use it, and probably nearly all apps.
2. No. I’m not sure why you’re so reluctant to depend on coroutines, but this is way too much complexity to support a basic Kotlin feature.
3. I would only do this if whatever JS code Kotlin generates for a suspend function is not what you want to expose directly to JS consumers.Zach Klippenstein (he/him) [MOD]
09/28/2025, 4:12 PMAll other targets (Jvm/Native) would simply call the blocking functions from theirDon’t do this, it’s bad practice for a suspend function to block its caller. Switch to an IO dispatcher in these functions to make the blocking call—either Dispatchers.IO directly or provide some configuration so your clients can specify the IO dispatcher.actual function definitionsxxxAsync()
russhwolf
09/28/2025, 6:01 PMMatt Nelson
09/28/2025, 7:17 PMcompileOnly dependencies sad panda
So
1. Suck it up and add the dependency to commonMain
2. Implement something akin to the below public API
// (Jvm/Native)
public actual sealed interface FileStream: Closeable {
// ...
public actual sealed interface Read: FileStream {
// ...
public actual suspend fun read(buf: ByteArray, offset: Int, len: Int, ctx: CoroutineContext = <http://Dispatchers.IO|Dispatchers.IO>): Int {
// default function just wraps blocking read call
return withContext(ctx) { read(buf, offset, len) }
}
}
// ...
}
// jsWasmJsMain (js/wasmJs)
public actual sealed interface FileStream: Closeable {
// ...
public actual sealed interface Read: FileStream {
// ...
public actual suspend fun read(buf: ByteArray, offset: Int, len: Int, ctx: CoroutineContext = EmptyCoroutineContext): Int
// No default. FileStream implementation wraps callback API in suspendCancellableCoroutine.
}
// ...
}Zach Klippenstein (he/him) [MOD]
09/28/2025, 9:47 PMZach Klippenstein (he/him) [MOD]
09/28/2025, 9:48 PMwithContext themselves, taking a context parameter is redundant.Matt Nelson
09/29/2025, 10:51 AMpublic suspend fun File.openReadAsync(ctx: CoroutineContext = <http://Dispatchers.IO|Dispatchers.IO>): FileStream.Read and then use the context in subsequent FileStream operationsMatt Nelson
10/01/2025, 7:43 PMkmp-file:file module.
// Alternatively use AsyncFs.Default which uses Dispatchers.IO (Dispatchers.Default on Js/WasmJs)
myScope.launch {
AsyncFs.of(ctx = myDispatcher).with {
SysTempDir
.resolve("some")
.resolve("path")
.resolve("..")
.resolve("thing")
.canonicalFile2Async()
.mkdirs2Async(mode = "700", mustCreate = true)
.resolve("hi.txt")
.let { file ->
file.openReadWriteAsync(excl = OpenExcl.MustCreate.of(mode = "400")).useAsync { stream ->
val expected = Array(50) { i ->
async {
val data = "Hello ${i}\n".encodeToByteArray()
stream.writeAsync(data)
data.size
}
}.toList().awaitAll().sum().toLong()
assertEquals(expected, stream.sizeAsync())
assertEquals(expected, stream.positionAsync())
stream.size(new = 0L)
assertEquals(0L, stream.positionAsync())
}
file.delete2Async(ignoreReadOnly = true)
}
}
}Wout Werkman
10/30/2025, 7:47 AMAsync is actually a somewhat confusing name in the world of structured concurrency. simply Suspending, NonBlocking, or whatever you want, as long as it does not suggest that any work "escapes the function".