Hello, I have a library that targets both Android ...
# javascript
t
Hello, I have a library that targets both Android and Js. It uses Metro for DI. I compile with
jsBrowserProductionLibraryDistribution
Copy code
js {
        moduleName = project.name
        useEsModules()
        binaries.library()
    }
I have the following code
Copy code
@Inject
internal class InvocationHandler(
    private val exposedFunctions: Map<String, Provider<ExposedFunction>>,
) {
    operator fun invoke(
        onInvocations: SharedFlow<Invocation>,
    ): Flow<Invocation> {
        return onInvocations
            .onEach {
                val exposedFunction = exposedFunctions[it.location] ?: return@onEach

                it.handle {
                    val fn = exposedFunction()
                    fn(it.parameters) // This throws an error on JS
                }
            }
    }
}

// ...

@Inject
@ContributesIntoMap(AppScope::class)
@ExposedAtLocation(ExposedFunctionLocation.IS_FILE_CACHED)
internal class IsFileCached(private val cacheRepository: CacheRepository) : ExposedFunction {
    override suspend fun invoke(args: List<Any>): Any {
        val fileUrl = args[0] as? String ?: return false
        return cacheRepository.isCached(fileUrl)
    }
}
For the code above, I will get
Copy code
TypeError: fn is not a function
And if I print with
fn.toString
Copy code
function (_this__u8e3s4, $completion) {
    return i.v8b(_this__u8e3s4, $completion);
  }
Do you know what could cause this issue?
a
I feel like the reason is that
exposedFunction
is an instance of a class with the invoke method. If you replace
fn
with
fn.invoke(it.parameters)
will it change something? If so, please, create an issue for this, because it definitely looks like a bug. If you need some guidenence on creating an issue for Kotlin, feel free to ping me.
e
Also, instead of only printing out
fn.toString()
, use something like
println("myMarker")
there, and inspect the generated JS. You'll find way more context that way. That's my strategy for compiled JS issues.
t
Thanks everyone. My guess is that the Metro compiler plugin could be inferring. My original code was
Copy code
exposedFunction()(it.parameters)
Looking at the JS produced
Copy code
suspendResult = exposedFunction.d61()(this.b8j_1.b7v_1, this);
For which I would get the error
Copy code
exposedFunction.d61(...) is not a function
Looking though the code generated, the reference
d61
only appears once. I tried different combination like the following
Copy code
val fn = exposedFunction()
fn(it.parameters) // fn is not a function

val invoker = fn::invoke
invoker(it.parameters) // this.s8i_1 is not a function
Regarding using
fn::invoke
the code generated would look like this
Copy code
function ExposedFunction$invoke$ref(p0) {
  this.s8i_1 = p0;
}
protoOf(ExposedFunction$invoke$ref).v8b = function (_this__u8e3s4, $completion) {
  return this.s8i_1(_this__u8e3s4, $completion);
};
// ----
          var exposedFunction = ensureNotNull(this.b8j_1.n83_1.g2(this.c8j_1.a7v_1));
          var fn = exposedFunction.d61();
          var invoker = ExposedFunction$invoke$ref_0(fn);
          this.l8_1 = 1;
          suspendResult = invoker(this.c8j_1.b7v_1, this);
          if (suspendResult === get_COROUTINE_SUSPENDED()) {
            return suspendResult;
          }
e
Which version of Kotlin are you using?
t
Using
kotlin = "2.2.20-RC2"
wanted to have
webMain
available 😊
e
Is
Invocation.handle
a suspend function?
t
Yes, this is the code
Copy code
suspend fun handle(handler: suspend () -> Any = {}) {
        if (hasStarted.compareAndExchange(expectedValue = false, newValue = true)) return
        _isHandled.complete(Unit)

        coroutineScope {
            launch {
                runCatching { handler() }
                    .onSuccess(completableDeferred::complete)
                    .onFailure(completableDeferred::completeExceptionally)
            }
        }
    }
e
What's the definition of the
Provider
type?
Ah found in Metro.
Copy code
public fun interface Provider<T> {
  public operator fun invoke(): T
}
What about the declaration of
ExposedFunction
? I was looking at having a minimal reproducer but so far all references are there.
Isn't that Metro's generated
Provider
implementation is not implementing the
invoke
function? Or generating it in the wrong way? That's the only explanation I can give now. Reproducing the code manually doesn't output anything wrong.
t
Thank you for looking at it! Let me try to provide you with a minimal repro.
gratitude thank you 1
I am happy! I was able to reproduce it. Build with
jsBrowserProductionLibraryDistribution
Then in `build/dist/js/productionLibrary`I added an
index.html
file:
Copy code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="module" src="shared.mjs"></script>
</head>
<body>
</body>
</html>
Open it, console,
Copy code
let m = await import('./shared.mjs')
new m.Greeting().greet()

// ERROR: getHelloPrefix is not a function
But could be because Metro uses reflection 🤔 I see on
MapKey
and
Multibinds
that the retention is at runtime.
Copy code
Reflection is not supported in JavaScript target; therefore, annotations cannot be read at runtime.
e
Oh nice, thanks. I'll give it a go soon. I'm just curious to understand if it's a Metro problem, or a Kotlin problem.
thank you color 1
Doesn't Metro claim to support all multiplatform targets?
t
Yes they do. Did not have any problem with the Wasm target. Funny thing here, I only target JS in the example provided, yet I had to OptIn with
ExperimentalWasmJsInterop::class
e
It's just easier to tag @Zac Sweers 👀 maybe there is a straightforward answer to the problem.
👋 1
z
Metro doesn’t use reflection at runtime
It may create instances of mapkey annotations but that’s all handled in code gen
Does JS not support instantiating annotation instances?
e
Yes yes, it does, since 1.6.something I believe.
z
Not sure what the issue is then, sorry. I’m not super familiar with JS :/
If you make a minimal failing repro I can take a look
t
I did share a small repro above, it uses
Copy code
kotlin = "2.2.20-RC2"
metro = "0.7.0-2.2.20-RC2-alpha06"
z
Sorry let me be more specific: file an issue with a link to said repro because I am on vacation and will look when I have time :)
🫡 1
e
There is a problem in the reproducer. I believe you wanted to write:
Copy code
val graph = createGraph<Graph>()
val provider = graph.exposedFunctions["something"] ?: error("No exposed function found")
val getHelloPrefix: ExposedFunction = provider.invoke()
val helloPrefix = getHelloPrefix.invoke(emptyList())
To "fix" the problem, explicitly cast to `GetHelloPrefix`:
Copy code
provider.invoke() as GetHelloPrefix
Working code:
Copy code
val graph = createGraph<Graph>()
val provider = graph.exposedFunctions["something"]!!
val getHelloPrefix: GetHelloPrefix = provider.invoke() as GetHelloPrefix
val helloPrefix = getHelloPrefix.invoke(emptyList())
t
Nice, unfortunately this solution is not applicable, because I cannot know the type in advance. Here
something
is hardcoded, but it can be any string 😞 So we have
GetHelloPrefix
but it could
GetBoomPrefix
etc
e
This is a Kotlin problem with extending a suspending function
Copy code
interface ExposedFunction : suspend (List<Any>) -> Any
Here is your issue https://youtrack.jetbrains.com/issue/KT-64821/ @Artem Kobzar your realm I believe 😄
z
ah, yes you can't extend function types in JS
t
YES! It works ❤️ Changed it for
Copy code
interface ExposedFunction {
    suspend operator fun invoke(args: List<Any>): Any
}
🆒 1
👍 2
e
(Upvote the issue btw, never wrong with it)
t
Yup, I was having this issue only compiling DevelopmentLibraryDistribution
Copy code
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
Thanks again everyone.
blob no problem 1