diff --git a/packages/graalvm/api/graalvm.api b/packages/graalvm/api/graalvm.api index e7a61f619..d4c6a46c9 100644 --- a/packages/graalvm/api/graalvm.api +++ b/packages/graalvm/api/graalvm.api @@ -5816,6 +5816,24 @@ public final synthetic class elide/runtime/javascript/$QueueMicrotaskCallable$In public fun isBuildable ()Z } +public synthetic class elide/runtime/javascript/$StructuredCloneBuiltin$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/$StructuredCloneBuiltin$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 instantiate ()Ljava/lang/Object; + public fun isBuildable ()Z +} + public final class elide/runtime/javascript/NavigatorBuiltin : elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic, elide/runtime/interop/ReadOnlyProxyObject { public fun ()V public fun getMember (Ljava/lang/String;)Ljava/lang/Object; @@ -5835,6 +5853,13 @@ public final class elide/runtime/javascript/QueueMicrotaskCallable : elide/runti public fun install (Lelide/runtime/intrinsics/GuestIntrinsic$MutableIntrinsicBindings;)V } +public final class elide/runtime/javascript/StructuredCloneBuiltin : elide/runtime/gvm/internals/intrinsics/js/AbstractJsIntrinsic, org/graalvm/polyglot/proxy/ProxyExecutable { + public fun ()V + public final fun clone (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Context;)Lorg/graalvm/polyglot/Value; + 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/javascript/StructuredCloneBuiltin.kt b/packages/graalvm/src/main/kotlin/elide/runtime/javascript/StructuredCloneBuiltin.kt new file mode 100644 index 000000000..7d94c7cd1 --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/javascript/StructuredCloneBuiltin.kt @@ -0,0 +1,77 @@ +/* + * 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 org.graalvm.polyglot.Context +import org.graalvm.polyglot.Source +import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable +import jakarta.inject.Singleton +import elide.runtime.core.DelicateElideApi +import elide.runtime.gvm.api.Intrinsic +import elide.runtime.gvm.internals.intrinsics.js.AbstractJsIntrinsic +import elide.runtime.gvm.js.JsError +import elide.runtime.gvm.js.JsSymbol.JsSymbols.asPublicJsSymbol +import elide.runtime.intrinsics.GuestIntrinsic + +// Name of the `structuredClone` function in the global scope. +private const val CLONE_FN_NAME = "structuredClone" + +// Public JavaScript symbol for the `structuredClone` function. +private val CLONE_FN_SYMBOL = CLONE_FN_NAME.asPublicJsSymbol() + +/** + * ## Structured Clone Built-in + * + * Implements the `structuredClone` global function for JavaScript, which is used to create a deep copy of an object, + * usually for the purpose of transferring it between different execution contexts. + * + * ### Standards Compliance + * + * The Navigator `structuredClone` function is defined as part of the WinterTC Minimum Common API. + * + * [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) + */ +@Singleton +@Intrinsic(CLONE_FN_NAME) public class StructuredCloneBuiltin : ProxyExecutable, AbstractJsIntrinsic() { + // language=JavaScript + private val structuredCloner = """ + function doStructuredClone(value) { + return JSON.parse(JSON.stringify(value)); + } + doStructuredClone; + """.trimIndent() + + private val cloner = Source.newBuilder("js", structuredCloner, "structuredCloner.js") + .internal(true) + .cached(true) + .build() + + override fun install(bindings: GuestIntrinsic.MutableIntrinsicBindings) { + bindings[CLONE_FN_SYMBOL] = this + } + + public fun clone(value: Value, forContext: Context): Value { + val cloner = forContext.eval(cloner) + assert(cloner.canExecute()) { "Structured cloner function is not executable" } + return requireNotNull(cloner.execute(value)) { "Failed to clone value" } + } + + override fun execute(vararg arguments: Value?): Any? { + val value = arguments.firstOrNull() ?: throw JsError.typeError("First argument to `structuredClone` is required") + if (value.canExecute()) throw JsError.typeError("Cannot clone functions") + return clone(value, value.context) + } +} 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 561262851..7db995b04 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 @@ -67,6 +67,7 @@ private const val ENABLE_SUPPRESSIONS = true "escape", "unescape", "navigator", + "structuredClone", "Navigator", "Object", "Function", @@ -171,7 +172,6 @@ private const val ENABLE_SUPPRESSIONS = true "setInterval", "setTimeout", "Storage", - "structuredClone", "SubtleCrypto", "DOMException", "TextDecoder", @@ -213,7 +213,6 @@ private const val ENABLE_SUPPRESSIONS = true private val allowMissingGlobals = sortedSetOf( "setImmediate", // not yet implemented "clearImmediate", // not yet implemented - "structuredClone", // not yet implemented "InternalError", // web-standard only, not present in non-browser runtimes "BroadcastChannel", // not yet implemented "CloseEvent", // not yet implemented diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/javascript/StructuredCloneTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/javascript/StructuredCloneTest.kt new file mode 100644 index 000000000..33ce76d18 --- /dev/null +++ b/packages/graalvm/src/test/kotlin/elide/runtime/javascript/StructuredCloneTest.kt @@ -0,0 +1,59 @@ +/* + * 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 org.graalvm.polyglot.Value +import org.junit.jupiter.api.assertNotNull +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotSame +import kotlin.test.assertTrue +import elide.annotations.Inject +import elide.runtime.core.DelicateElideApi +import elide.runtime.gvm.internals.js.AbstractJsIntrinsicTest +import elide.runtime.plugins.js.javascript +import elide.testing.annotations.Test +import elide.testing.annotations.TestCase + +@TestCase internal class StructuredCloneTest : AbstractJsIntrinsicTest() { + @Inject lateinit var structuredClone: StructuredCloneBuiltin + override fun provide(): StructuredCloneBuiltin = structuredClone + + override fun testInjectable() { + assertNotNull(structuredClone) + } + + @Test fun testCloneSimple() { + val (value, cloned) = withContext { + val value = javascript( + // language=JavaScript + """ + const x = { a: 1, b: 2 }; + x; + """.trimIndent() + ) + value to structuredClone.clone(Value.asValue(value), unwrap()) + } + assertNotNull(cloned) + assertTrue(cloned.hasMembers()) + assertTrue(cloned.hasMember("a")) + assertTrue(cloned.hasMember("b")) + assertFalse(cloned.hasMember("c")) + assertEquals(1, cloned.getMember("a").asInt()) + assertEquals(2, cloned.getMember("b").asInt()) + assertEquals(2, cloned.memberKeys.size) + assertNotSame(value, cloned) + } +} diff --git a/packages/graalvm/src/test/kotlin/elide/runtime/winter/CommonMinimumTest.kt b/packages/graalvm/src/test/kotlin/elide/runtime/winter/CommonMinimumTest.kt index 82ca48535..7ef6925f0 100644 --- a/packages/graalvm/src/test/kotlin/elide/runtime/winter/CommonMinimumTest.kt +++ b/packages/graalvm/src/test/kotlin/elide/runtime/winter/CommonMinimumTest.kt @@ -66,7 +66,6 @@ private const val ENABLE_SUPPRESSIONS = true "TextDecoderStream", "TextEncoderStream", "URLPattern", - "structuredClone", ) // Minimum Common API ยง3.1: Interfaces.