diff --git a/packages/engine/src/main/kotlin/elide/runtime/core/PolyglotContext.kt b/packages/engine/src/main/kotlin/elide/runtime/core/PolyglotContext.kt index 745bc4aeb..eb5156f72 100644 --- a/packages/engine/src/main/kotlin/elide/runtime/core/PolyglotContext.kt +++ b/packages/engine/src/main/kotlin/elide/runtime/core/PolyglotContext.kt @@ -176,7 +176,7 @@ public interface PolyglotContext { * * @param source The guest code to be executed. * @param internals Whether to allow access to internal runtime features; the provided [source] must be marked as - * internal to enable this acesss. + * internal to enable this access. * @return The result of evaluating the [source]. */ public fun evaluate(source: Source, internals: Boolean): PolyglotValue = diff --git a/packages/graalvm/api/graalvm.api b/packages/graalvm/api/graalvm.api index 1e0b12386..b4a6592be 100644 --- a/packages/graalvm/api/graalvm.api +++ b/packages/graalvm/api/graalvm.api @@ -517,6 +517,14 @@ public final class elide/runtime/gvm/internals/intrinsics/ElideIntrinsicKt { public static final fun installElideBuiltin (Ljava/lang/String;Ljava/lang/Object;)V } +public abstract class elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic : elide/runtime/intrinsics/GuestIntrinsic { + public fun ()V + public fun displayName ()Ljava/lang/String; + public fun language ()Lelide/runtime/gvm/GuestLanguage; + public fun symbolicName ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + public synthetic class elide/runtime/gvm/internals/intrinsics/js/abort/$AbortControllerIntrinsic$Definition : io/micronaut/context/AbstractInitializableBeanDefinitionAndReference { public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; public fun ()V @@ -5771,6 +5779,29 @@ public abstract interface class elide/runtime/intrinsics/testing/TestingAPI$Test public abstract interface class elide/runtime/intrinsics/testing/TestingAPI$TestGraphNode$Test : elide/runtime/intrinsics/testing/TestingAPI$TestGraphNode { } +public synthetic class elide/runtime/javascript/$QueueMicrotaskCallable$Definition : io/micronaut/context/AbstractInitializableBeanDefinitionAndReference { + public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; + public fun ()V + protected fun (Ljava/lang/Class;Lio/micronaut/context/AbstractInitializableBeanDefinition$MethodOrFieldReference;)V + public fun instantiate (Lio/micronaut/context/BeanResolutionContext;Lio/micronaut/context/BeanContext;)Ljava/lang/Object; + public fun isEnabled (Lio/micronaut/context/BeanContext;)Z + public fun isEnabled (Lio/micronaut/context/BeanContext;Lio/micronaut/context/BeanResolutionContext;)Z + public fun load ()Lio/micronaut/inject/BeanDefinition; +} + +public final synthetic class elide/runtime/javascript/$QueueMicrotaskCallable$Introspection : io/micronaut/inject/beans/AbstractInitializableBeanIntrospectionAndReference { + public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; + public fun ()V + public fun hasBuilder ()Z + public fun isBuildable ()Z +} + +public final class elide/runtime/javascript/QueueMicrotaskCallable : elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic, org/graalvm/polyglot/proxy/ProxyExecutable { + public fun (Lelide/runtime/exec/GuestExecutorProvider;)V + public fun execute ([Lorg/graalvm/polyglot/Value;)Ljava/lang/Object; + public fun install (Lelide/runtime/intrinsics/GuestIntrinsic$MutableIntrinsicBindings;)V +} + public synthetic class elide/runtime/node/asserts/$NodeAssertModule$Definition : io/micronaut/context/AbstractInitializableBeanDefinitionAndReference { public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; public fun ()V diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic.kt index 0a0e762a9..878a531ab 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic.kt @@ -17,7 +17,7 @@ import elide.runtime.gvm.GraalVMGuest import elide.runtime.intrinsics.GuestIntrinsic /** Abstract base class for all intrinsic implementations. */ -internal abstract class AbstractJsIntrinsic : GuestIntrinsic { +public abstract class AbstractJsIntrinsic : GuestIntrinsic { override fun language(): GuestLanguage = GraalVMGuest.JAVASCRIPT override fun symbolicName(): String = "native code" @Deprecated("Use symbolicName instead", ReplaceWith("symbolicName")) diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/javascript/QueueMicrotaskCallable.kt b/packages/graalvm/src/main/kotlin/elide/runtime/javascript/QueueMicrotaskCallable.kt new file mode 100644 index 000000000..c62bdefe4 --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/javascript/QueueMicrotaskCallable.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ +@file:OptIn(DelicateElideApi::class) + +package elide.runtime.javascript + +import elide.runtime.exec.GuestExecutorProvider +import elide.runtime.gvm.api.Intrinsic +import elide.runtime.gvm.internals.intrinsics.js.AbstractJsIntrinsic +import elide.runtime.gvm.js.JsError +import elide.runtime.intrinsics.GuestIntrinsic +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable +import elide.runtime.core.DelicateElideApi +import elide.runtime.gvm.js.JsSymbol.JsSymbols.asPublicJsSymbol +import elide.runtime.gvm.js.undefined + +// Name of the `queueMicrotask` function in the global scope. +private const val QUEUE_MICROTASK_NAME = "queueMicrotask" + +// Public JavaScript symbol for the `queueMicrotask` function. +private val QUEUE_MICROTASK_SYMBOL = QUEUE_MICROTASK_NAME.asPublicJsSymbol() + +/** + * ## Queue Microtask Callable + * + * Mounts a callable intrinsic function at the name `queueMicrotask`, in compliance with Web JavaScript standards which + * expect this function to be available in the global scope. The `queueMicrotask` function is used to queue a chunk of + * code to execute safely on the JavaScript event loop. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask) + */ +@Singleton +@Intrinsic(QUEUE_MICROTASK_NAME) public class QueueMicrotaskCallable @Inject constructor ( + private val executorProvider: GuestExecutorProvider, +) : ProxyExecutable, AbstractJsIntrinsic() { + override fun install(bindings: GuestIntrinsic.MutableIntrinsicBindings) { + bindings[QUEUE_MICROTASK_SYMBOL] = this + } + + internal operator fun invoke(callable: () -> Unit) { + executorProvider.executor().execute { + callable.invoke() + } + } + + override fun execute(vararg arguments: Value?): Any? { + val first = arguments.firstOrNull() ?: throw JsError.typeError("First argument to `queueMicrotask` is required") + if (!first.canExecute()) throw JsError.typeError("First argument to `queueMicrotask` must be a function") + invoke(first::executeVoid) + return undefined() + } +} diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/plugins/js/Extensions.kt b/packages/graalvm/src/main/kotlin/elide/runtime/plugins/js/Extensions.kt index bc65e8eab..0d2996875 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/plugins/js/Extensions.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/plugins/js/Extensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -61,7 +61,7 @@ import elide.runtime.core.evaluate * [PolyglotContext.evaluate] and selecting [JavaScript] as source language. * * @param source The interpreted JavaScript source code to be executed. - * @return The result of the invocation. If [esm] is `true`, an object is returned, with exported values as members. + * @return The result of the invocation; an object is returned, with exported values as members. */ @DelicateElideApi public fun PolyglotContext.javascript(source: Source): PolyglotValue = evaluate(source) diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/intrinsics/js/JsGlobalsTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/intrinsics/js/JsGlobalsTest.kt index 031cb3a54..6268150fa 100644 --- a/packages/graalvm/src/test/kotlin/elide/runtime/intrinsics/js/JsGlobalsTest.kt +++ b/packages/graalvm/src/test/kotlin/elide/runtime/intrinsics/js/JsGlobalsTest.kt @@ -59,6 +59,7 @@ private const val ENABLE_SUPPRESSIONS = true "isNaN", "parseFloat", "parseInt", + "queueMicrotask", "decodeURI", "decodeURIComponent", "encodeURI", @@ -158,7 +159,6 @@ private const val ENABLE_SUPPRESSIONS = true "PerformanceObserverEntryList", "performance", "process", - "queueMicrotask", "ReadableByteStreamController", "ReadableStream", "ReadableStreamBYOBReader", @@ -214,7 +214,6 @@ private const val ENABLE_SUPPRESSIONS = true "navigator", // not yet implemented "setImmediate", // not yet implemented "clearImmediate", // not yet implemented - "queueMicrotask", // not yet implemented "structuredClone", // not yet implemented "InternalError", // web-standard only, not present in non-browser runtimes "BroadcastChannel", // not yet implemented diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/javascript/QueueMicrotaskTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/javascript/QueueMicrotaskTest.kt new file mode 100644 index 000000000..60ab4eeed --- /dev/null +++ b/packages/graalvm/src/test/kotlin/elide/runtime/javascript/QueueMicrotaskTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ +package elide.runtime.javascript + +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue +import elide.annotations.Inject +import elide.runtime.core.DelicateElideApi +import elide.runtime.exec.GuestExecution +import elide.runtime.exec.GuestExecutorProvider +import elide.runtime.gvm.internals.js.AbstractJsIntrinsicTest +import elide.runtime.gvm.js.undefined +import elide.runtime.intrinsics.js.err.TypeError +import elide.runtime.plugins.js.javascript +import elide.testing.annotations.Test +import elide.testing.annotations.TestCase + +@TestCase internal class QueueMicrotaskTest : AbstractJsIntrinsicTest() { + @Inject lateinit var queueMicrotask: QueueMicrotaskCallable + override fun provide(): QueueMicrotaskCallable = queueMicrotask + + @Test override fun testInjectable() { + assertNotNull(queueMicrotask) + } + + @Test fun testExecMicrotask() { + val exec = GuestExecution.direct() + val prov = GuestExecutorProvider { exec } + val fresh = QueueMicrotaskCallable(prov) + var didExec = false + val invocable = { didExec = true } + assertDoesNotThrow { fresh.invoke(invocable) } + assertTrue(didExec) + } + + @Test fun testExecMicrotaskGuest() = dual { + val exec = GuestExecution.direct() + val prov = GuestExecutorProvider { exec } + val fresh = QueueMicrotaskCallable(prov) + var didExec = false + val invocable = { didExec = true } + assertDoesNotThrow { fresh.invoke(invocable) } + assertTrue(didExec) + }.guest { + // language=JavaScript + """ + let didExec = false; + queueMicrotask(() => didExec = true); + test(didExec).isEqualTo(true); + """ + } + + @OptIn(DelicateElideApi::class) + @Test fun testExecMicrotaskGuestDirect() { + val exec = GuestExecution.direct() + val prov = GuestExecutorProvider { exec } + val fresh = QueueMicrotaskCallable(prov) + val guestFn = withContext { + javascript( + // language=JavaScript + """ + const fn = (() => { + // hello + }); + fn; + """ + ) + } + + assertNotNull(guestFn) + assertDoesNotThrow { fresh.execute(guestFn) } + } + + @Test fun testExecMicrotaskRejectsNulls() { + assertThrows { queueMicrotask.execute(Value.asValue(null)) } + } + + @Test fun testExecMicrotaskRejectsNonExecutable() { + assertThrows { queueMicrotask.execute(Value.asValue(5)) } + } +}