Skip to content

Commit

Permalink
feat(graalvm): add structuredClone support
Browse files Browse the repository at this point in the history
feat(graalvm): add `structuredClone` support
test(graalvm): add `structuredClone` tests

Closes #1267

Relates-to: #1267
Signed-off-by: Sam Gammon <sam@elide.dev>
  • Loading branch information
sgammon committed Mar 10, 2025
1 parent d040721 commit d54a8fe
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 3 deletions.
25 changes: 25 additions & 0 deletions packages/graalvm/api/graalvm.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
protected fun <init> (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 <init> ()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 <init> ()V
public fun getMember (Ljava/lang/String;)Ljava/lang/Object;
Expand All @@ -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 <init> ()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 <init> ()V
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private const val ENABLE_SUPPRESSIONS = true
"escape",
"unescape",
"navigator",
"structuredClone",
"Navigator",
"Object",
"Function",
Expand Down Expand Up @@ -171,7 +172,6 @@ private const val ENABLE_SUPPRESSIONS = true
"setInterval",
"setTimeout",
"Storage",
"structuredClone",
"SubtleCrypto",
"DOMException",
"TextDecoder",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StructuredCloneBuiltin>() {
@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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ private const val ENABLE_SUPPRESSIONS = true
"TextDecoderStream",
"TextEncoderStream",
"URLPattern",
"structuredClone",
)

// Minimum Common API §3.1: Interfaces.
Expand Down

0 comments on commit d54a8fe

Please sign in to comment.