I am just trying to understand how to use the APIs...
# webassembly
m
I am just trying to understand how to use the APIs provided by the kotlin-wrappers project. As an example I have picked the kotlin-jszip module. I tried to convert the initial example given in https://stuk.github.io/jszip/ to Kotlin but somehow I got stuck. It took me already quite some time to find out about this “unsafeJso” function. I finally failed to convert the async stuff. Can anybody help me with that. Code so far inside the thread.
Copy code
import js.objects.unsafeJso
import jszip.JSZip
import kotlin.test.Test

// See: <https://github.com/JetBrains/kotlin-wrappers>
// and <https://stuk.github.io/jszip/>

class ZipWrapperTest {
    private val imgData = "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=" // smile.gif as Base64.

    @Test
    fun test() {
        val zip = JSZip()
        zip.file("Hello.txt", "Hello World\n")
        val img = checkNotNull(zip.folder("images"))
        img.file("smile.gif", imgData, unsafeJso { base64 = true })

// ???
//        zip.generateAsync({type:"blob"})
//            .then(function(content) {
//                // see FileSaver.js
//                saveAs(content, "example.zip");
//            });
    }
}
k
My wizards are implemented with the kotlin-wrappers and use jszip: https://github.com/terrakok/Compose-Multiplatform-Wizard
you can take a look
t
Should work like this:
Copy code
val options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
        zip.generateAsync(options)
            .then { saveAs(it, "example.zip") }
Or with coroutines:
Copy code
val options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
        val content = zip.generate(options)
        saveAs(content, "example.zip") }
m
@turansky Where does the
saveAs
come from? I can’t find it.
t
I thought it's your local function 😉
From
FileSaver.js
m
No, but maybe the original example was expecting such a function. I thought it came from the framework.
t
Probably this
Which browsers you support?
m
Hopefully all (Firefox, Chrome, Safari) but at the moment I was just running a local test.
t
showSaveFilePicker can be used for fast check in Chrome
m
@turansky Is it possible you have messed up the file pickers? The file https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-browser/src/commonMain/kotlin/web/fs/showSaveFilePicker.kt , which I would need here because I want to save and not open a ZIP file, actually contains the
showOpenFilePicker
. So currently there does not seem to be an option to get a file picker for saving a file.
t
Fixed! Thank you for the report.
m
With the fixes applied I get
Copy code
val promise: Promise<FileSystemFileHandle> = showSaveFilePickerAsync(options)
but what have I to do next to actually write the content Blob to a file. As I said already, I don’t have a JavaScript background.
m
And why does the file picker only seem to work in Chrome but not in Firefox?
@Konstantin Tskhovrebov I am just looking into your solution too.
t
> And why does the file picker only seem to work in Chrome but not in Firefox? It's Chrome API, which they didn't discussed with other browsers 😞
m
I finally ended up with the following code which compiles and runs without problem (in Chrome) but in the end just produces an empty zip file. What’s wrong with it?
Copy code
import js.objects.unsafeJso
import js.promise.Promise
import js.promise.await
import jszip.JSZip
import jszip.JSZipGeneratorOptions
import jszip.OutputType
import jszip.blob
import jszip.generate
import web.blob.Blob
import web.experimental.ExperimentalWebApi
import web.fs.FileSystemFileHandle
import web.fs.SaveFilePickerOptions
import web.fs.createWritable
import web.fs.write

actual suspend fun createZip(imgData: String) {
    println("Creating ZIP file.")
    val zip = JSZip()
    zip.file("Hello.txt", "Hello World\n")
    val img = checkNotNull(zip.folder("images"))
    img.file("smile.gif", imgData, unsafeJso { base64 = true })
    val options: JSZipGeneratorOptions<Blob> = unsafeJso { type = OutputType.blob }
    val content: Blob = zip.generate(options)
    saveZipFileAs(content, "example.zip")
}

@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
    println("Saving ZIP file.")
    val options: SaveFilePickerOptions = unsafeJso { suggestedName = fileName }
    val f: FileSystemFileHandle = showSaveFilePickerAsync(options).await()
    f.createWritable().write(content)
}

/**
 * [MDN Reference](<https://developer.mozilla.org/docs/Web/API/Window/showSaveFilePicker>)
 */
@ExperimentalWebApi
@JsName("showSaveFilePicker")
external fun showSaveFilePickerAsync(
    options: SaveFilePickerOptions = definedExternally,
): Promise<FileSystemFileHandle>
t
Copy code
val w = f.createWritable()
w.write(content)
w.close()
Close call required
I also wanted to introduce suspend
showSaveFilePicker
, but I thought, that it will be weird if you will receive error on cancellation. But it's what you will have now with
await
:(
m
It works now, thanks a lot, but why didn’t you make the FileSystemWritableFileStream AutoCloseable? Then you could wrap that in a .use block and won’t forget the close() call.
t
suspend
close
? 😉
m
Life with JavaScript is hard :-(
t
Could you, please report issue in Kotlin Wrappers?
m
Which issue do you mean?
t
Missed
.use
support for
FileSystemWritableFileStream
🆗 1
m
t
m
This is the most idiomatic solution I could come up with so far which also handles the abort case properly. It still looks a bit strange to me.
Copy code
import js.objects.unsafeJso
import js.promise.Promise
import jszip.JSZip
import jszip.OutputType
import jszip.blob
import jszip.generate
import web.blob.Blob
import web.experimental.ExperimentalWebApi
import web.fs.FileSystemFileHandle
import web.fs.SaveFilePickerOptions
import web.fs.createWritable
import web.fs.write
import web.streams.close

actual suspend fun createZip(imgData: String) {
    JSZip().apply {
        file("Hello.txt", "Hello World\n")
        file("images/smile.gif", imgData, unsafeJso { base64 = true })
        saveZipFileAs(generate(unsafeJso { type = OutputType.blob }), "example.zip")
    }
}

@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
    var fh: FileSystemFileHandle? = null
    showSaveFilePickerAsync(unsafeJso { suggestedName = fileName }).then( { f -> fh = f; f }, { r -> r } ).await()
    fh?.let {it.createWritable().apply {
        write(content)
        close()
    }}
}

/**
 * [MDN Reference](<https://developer.mozilla.org/docs/Web/API/Window/showSaveFilePicker>)
 */
@ExperimentalWebApi
@JsName("showSaveFilePicker")
external fun showSaveFilePickerAsync(
    options: SaveFilePickerOptions = definedExternally,
): Promise<FileSystemFileHandle>
t
Looks like magic 🙂
Copy code
@OptIn(ExperimentalWebApi::class)
private suspend fun saveZipFileAs(content: Blob, fileName: String) {
    val fh = showSaveFilePickerAsync(unsafeJso { suggestedName = fileName })
        .catch { _ -> null }
        .await()
        ?: return
 
    fh.createWritable().apply {
        write(content)
        close()
    }
}
But real errors will be ignored in that case 😞
m
At least your last code fragment fixes my above code again so that it works now. Nevertheless I find these JavaScript APIs very error-prone, not very intuitive and not easily readable.
t
It requires sugar, like
.use
extension or something similar if we are talking about writable.
m
I have fixed my code above too. There was just one .await() missing. Now it works.
Do you see a chance to add FileSaver.js to the Kotlin-Wrappers because it seems to be more cross-platform?
t
We accept PRs 😉
🤔 1
1 step - subproject creation Name -
kotlin-file-saver
Example
m
@Konstantin Tskhovrebov I have now downloaded your file-saver import from https://github.com/terrakok/Compose-Multiplatform-Wizard/blob/master/src/jsMain/kotlin/npm/FileSaverJs.kt and after adjusting it slightly to be also usable from wasmJs, saving files with a nice Dialog now works like a charm on Firefox, Chrome and Safari. (It has to be configured in the browser too to always ask for the file location to get the dialog.) Thanks a lot for the hint!
t
Looks like missed PRs! 😉
👌 1
Also we will check if
close()
call is required if error will occur in
write
process.
Copy code
fh.createWritable().apply {
    write(content) 
    // no close call if cancellation or error will occur
    close()
}
m
How should a cancellation happen in this context? If the file selection is cancelled then we won’t get here.
t
Looks like add/use FileSaver.js isn't good idea 😞
Better option for now:
Copy code
import web.blob.Blob
import web.dom.document
import web.html.HtmlTagName.a
import web.url.URL

/**
 * Downloads the given content as a file.
 * @param content - The file content.
 * @param fileName - The proposed filename.
 */
fun downloadFile(
    content: Blob,
    fileName: String,
) {
    val data = URL.createObjectURL(content)

    val anchor = document.createElement(a)
    anchor.href = data
    anchor.download = fileName
    anchor.style.display = "none"

    document.body.appendChild(anchor)
    anchor.click()
    document.body.removeChild(anchor)
}
Original
m
The web-technologies are full of surprises 🤔, but it works and has the advantage of avoiding an external dependency. Will you make that part of kotlin-wrappers?
t
Web technologies are moving fast. To support all new use cases (like iOS web app) we will need additional checks.
has the advantage of avoiding an external dependency
There is no such goal in common. In current case we have unsupported library with known problems, which can be fixed in 2 lines of code.
"unstable" markers will be added later 😉
m
Maybe you could add this comment to
downloadFile
in order to avoid confusion.
Copy code
* By default, the file is directly downloaded to the default download
* directory of the browser. If you want to select the download location via a file selector,
* then you have to configure that behaviour in your browser settings.
t
PR? ;)
m
I have written a self-contained test to play a bit with the kotlin-jszip API. The test runs but to me the code looks very awkward. Especially the type conversions look complicated and inefficient. Maybe this code helps someone else to unterstand this API and maybe someone has an idea how to improve it.
Copy code
package de.mpmediasoft.kotlinwrappers.jszip

import js.buffer.ArrayBuffer
import js.core.JsPrimitives.toByte
import js.core.JsPrimitives.toJsByte
import js.objects.unsafeJso
import js.promise.await
import js.typedarrays.Uint8Array
import jszip.*
import kotlinx.coroutines.test.runTest
import web.blob.Blob
import kotlin.test.Test
import kotlin.test.assertTrue

// See: <https://github.com/JetBrains/kotlin-wrappers>
// and <https://stuk.github.io/jszip/>

private fun Uint8Array<*>.toByteArray(): ByteArray = ByteArray(length) { index -> at(index)!!.toByte() }

private fun ByteArray.toUint8Array(): Uint8Array<*> = Uint8Array<ArrayBuffer>(size).apply { forEachIndexed { index, byte -> set(index, byte.toJsByte()) } }

class KotlinWrappersJsZipTest {

    private val imgData = "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=" // smile.gif as Base64.

    private suspend fun createZipFileContent(): Blob {
        JSZip().apply {
            file("Hello.txt", "Hello World\nAnd this is the second line.\nAnd one more.")
            file("images/smile.gif", imgData, unsafeJso { base64 = true })
            return generate(unsafeJso { type = OutputType.blob })
        }
    }

    @Test
    fun testConsume() = runTest {
        // Create some test data:

        val zipFileContent: Blob = createZipFileContent()
        println("zipFileContent.size = ${zipFileContent.size.toInt()}\n")
        assertTrue(zipFileContent.size.toInt() > 0)

        // Examine ZIP data and extract some values:

        val js = JSZip()

        js.load(zipFileContent)
        js.forEach { p,f ->
//            println("$p -> ${f.name}, ${f.date}, ${f.comment}\n") // Just crashes when I try to print f.comment
            println("$p -> ${f.name}, ${f.date}\n")
        }

        val textEntry: String = js.file("Hello.txt")!!.async(OutputType.text).await().toString()
        assertTrue(textEntry.length > 0)
        println("The file 'Hello.txt' contains the text:\n$textEntry\n")

        val gifEntry : ByteArray = js.file("images/smile.gif")!!.async(OutputType.uint8array).await().toByteArray()
        assertTrue(gifEntry.size > 0)

        // Just to see whether we can also convert back.
        val gifEntryJs: Uint8Array<*> = gifEntry.toUint8Array()
        assertTrue(gifEntryJs.length > 0)
        assertTrue(gifEntry.size == gifEntryJs.length)
    }

}
t
Similar adapters can be added for
JSZipObject
> Especially the type conversions look complicated and inefficient Do you mean conversion
Uint8Array
->
ByteArray
?
👌 1
Both questions are missed PRs 😞 People write adapters locally, but don't share...
@Michael Paus could you please report issue about
Uint8Array
-
ByteArray
conversion?
m
👌 1
t
> but to me the code looks very awkward. PR which I expect in such cases (to help other users) 😉 cc @Michael Paus
JSZipObject
and typed array adapters available in Kotlin Wrappers
2025.8.4
! 😜
m
t
@Michael Paus Why it's not PR? 😉
m
@turansky 1. Because I am used to discuss changes in an issue before I start to implement them (it would be wasted time if you just reject them or point me to something I have missed) and 2. because I still do not fully understand your type hierarchy and how it differs between the JS and the wasmJs target and the new common web folder.
t
1. It will be synchronous with existed
ByteArray
<->
UByteArray
conversion methods - let's add them 2. I also wasn't familiar with byte arrays before your request 😉
@Michael Paus we added requested Kotlin sugar for
JSZip
and
ByteArray
- it will be available in next release. Feedback is welcome!
🙏 1
K 1
@Michael Paus we need good name for fun interface with suspend
close
to solve issue about
.use
😉 Do you have any variants?
m
Oh dear, I am not good in such things 🤔. What about .suse 😉 (*sus*pend + use)
t
Copy code
fun interface ...Closable {
    suspend fun close()
}
we need name for interface
m
Java has Closeable and AutoCloseable, Kotlin has AutoCloseable. What about SelfCloseable or SuspendCloseable? (Note the spelling with an ‘e’ to be consistent.)
Now with 2025.8.11 and
Copy code
val content: Blob = ...
var fh: FileSystemFileHandle? = …
I was hoping to be able to just replace
Copy code
fh?.createWritable()?.apply {
	write(content)
	close()
}
by
Copy code
fh?.createWritable()?.use {
	write(content)
}
but that doesn’t compile.
t
We copied standard
use
signature. Writer - first parameter of lambda
use { writer -> ... }
m
OK. I thought it would behave like the apply block. This does compile.
Copy code
fh?.createWritable()?.use { w ->
    w.write(content)
}
Or this.
Copy code
fh?.createWritable()?.use {
    it.write(content)
}
Thank you.
t
use
- "safe"
let
👌 1
@Michael Paus thank you for the issues! Please write us (or create PR) if you will find any new problem.
🆗 1