Tristan
09/04/2025, 6:17 PMjsBrowserProductionLibraryDistribution
js {
moduleName = project.name
useEsModules()
binaries.library()
}
I have the following 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
TypeError: fn is not a function
And if I print with fn.toString
function (_this__u8e3s4, $completion) {
return i.v8b(_this__u8e3s4, $completion);
}
Do you know what could cause this issue?Artem Kobzar
09/05/2025, 5:22 AMexposedFunction
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.Edoardo Luppi
09/05/2025, 9:35 AMfn.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.Tristan
09/05/2025, 4:37 PMexposedFunction()(it.parameters)
Looking at the JS produced
suspendResult = exposedFunction.d61()(this.b8j_1.b7v_1, this);
For which I would get the error
exposedFunction.d61(...) is not a function
Looking though the code generated, the reference d61
only appears once.
I tried different combination like the following
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
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;
}
Edoardo Luppi
09/05/2025, 4:38 PMTristan
09/05/2025, 4:40 PMkotlin = "2.2.20-RC2"
wanted to have webMain
available 😊Edoardo Luppi
09/05/2025, 4:43 PMInvocation.handle
a suspend function?Tristan
09/05/2025, 4:45 PMsuspend 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)
}
}
}
Edoardo Luppi
09/05/2025, 5:06 PMProvider
type?Edoardo Luppi
09/05/2025, 5:07 PMpublic fun interface Provider<T> {
public operator fun invoke(): T
}
Edoardo Luppi
09/05/2025, 5:19 PMExposedFunction
?
I was looking at having a minimal reproducer but so far all references are there.Edoardo Luppi
09/05/2025, 5:44 PMProvider
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.Tristan
09/05/2025, 5:52 PMTristan
09/05/2025, 6:33 PMjsBrowserProductionLibraryDistribution
Then in `build/dist/js/productionLibrary`I added an index.html
file:
<!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,
let m = await import('./shared.mjs')
new m.Greeting().greet()
// ERROR: getHelloPrefix is not a function
Tristan
09/05/2025, 6:34 PMMapKey
and Multibinds
that the retention is at runtime.
Reflection is not supported in JavaScript target; therefore, annotations cannot be read at runtime.
Edoardo Luppi
09/05/2025, 6:34 PMEdoardo Luppi
09/05/2025, 6:35 PMTristan
09/05/2025, 6:36 PMExperimentalWasmJsInterop::class
Edoardo Luppi
09/05/2025, 6:37 PMZac Sweers
09/05/2025, 6:39 PMZac Sweers
09/05/2025, 6:39 PMZac Sweers
09/05/2025, 6:40 PMEdoardo Luppi
09/05/2025, 6:41 PMTristan
09/05/2025, 6:41 PMZac Sweers
09/05/2025, 6:41 PMZac Sweers
09/05/2025, 6:42 PMTristan
09/05/2025, 6:43 PMkotlin = "2.2.20-RC2"
metro = "0.7.0-2.2.20-RC2-alpha06"
Zac Sweers
09/05/2025, 6:50 PMEdoardo Luppi
09/05/2025, 6:56 PMval graph = createGraph<Graph>()
val provider = graph.exposedFunctions["something"] ?: error("No exposed function found")
val getHelloPrefix: ExposedFunction = provider.invoke()
val helloPrefix = getHelloPrefix.invoke(emptyList())
Edoardo Luppi
09/05/2025, 6:57 PMprovider.invoke() as GetHelloPrefix
Edoardo Luppi
09/05/2025, 6:58 PMval graph = createGraph<Graph>()
val provider = graph.exposedFunctions["something"]!!
val getHelloPrefix: GetHelloPrefix = provider.invoke() as GetHelloPrefix
val helloPrefix = getHelloPrefix.invoke(emptyList())
Tristan
09/05/2025, 7:01 PMsomething
is hardcoded, but it can be any string 😞 So we have GetHelloPrefix
but it could GetBoomPrefix
etcEdoardo Luppi
09/05/2025, 7:01 PMinterface ExposedFunction : suspend (List<Any>) -> Any
Here is your issue https://youtrack.jetbrains.com/issue/KT-64821/
@Artem Kobzar your realm I believe 😄Zac Sweers
09/05/2025, 7:03 PMTristan
09/05/2025, 7:03 PMinterface ExposedFunction {
suspend operator fun invoke(args: List<Any>): Any
}
Edoardo Luppi
09/05/2025, 7:05 PMTristan
09/05/2025, 7:09 PMUncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
Thanks again everyone.