Need API advice. Have a library, <kmp-file> , and...
# library-development
m
Need API advice. Have a library, kmp-file , and want to add an async API in order to support more Js/WasmJs functionality. • Currently, only supporting use of
Node.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...
z
1. Yes. You don’t need another dependency to expose a suspend function api. Basic suspend functions/Continuation is built into the core standard lib. You need it to implement that api, but that’s an
implementation
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.
All other targets (Jvm/Native) would simply call the blocking functions from their
xxxAsync()
actual function definitions
Don’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.
1
r
I know we all got scarred by the RxJava to coroutines migration, but at this point it's hard to imagine a coroutines alternative coming along that becomes more core to the ecosystem than kotlinx.coroutines already is. If Swift Export is willing to add a kotlinx dependency, it's getting really hard to justify avoiding it as a third-party library.
m
Very good points, TY Zach and Russell. I wish there was a way for non-Jvm to take advantage of
compileOnly
dependencies sad panda So 1. Suck it up and add the dependency to
commonMain
2. Implement something akin to the below public API
Copy code
// (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) }
        }
    }

    // ...
}
Copy code
// 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.
    }

    // ...
}
z
Do you really need those interfaces to be expect/actual?
The pattern libraries like retrofit use let you configure the default IO dispatcher/executor when creating the client. If your caller wants to change other aspects of the coroutine context at the call site they can just use
withContext
themselves, taking a context parameter is redundant.
m
> Do you really need those interfaces to be expect/actual? Yes > The pattern libraries like retrofit use let you configure the default IO dispatcher/executor when creating the client. Good point. Will do something like
public suspend fun File.openReadAsync(ctx: CoroutineContext = <http://Dispatchers.IO|Dispatchers.IO>): FileStream.Read
and then use the context in subsequent
FileStream
operations
Things turned out great. Was able to implement it as an extension module with minimal hooks needed in the
kmp-file:file
module.
Copy code
// 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)
            }
    }
}
w
If you have legacy blocking APIs, then in the perfect scenario you can get away with adding suspend overloads without breaking compatibility. If that doesn't work, and you need another name, then
Async
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".